第六阶段开发。

This commit is contained in:
zfjia committed 2026-11-12 13:36:24 +08:00
1 parent 35751b4f00
commit bacdd15f3a
93 files changed
+10190 -22

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
-- =============================================================================
-- 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-<endpointId 后缀>',
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:<extension>@<ip>:<port>',
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;
+129
View File
@@ -0,0 +1,129 @@
-- =============================================================================
-- SIP 认证(apt_sip_auth)
-- 定位:分机在 Asterisk 上的 auth 账号,与 endpoint 1:1(可先建认证后绑分机)。
-- 多租户/多实例:
-- * 唯一性 = (gyid, instance_code, username) —— 同一实例下用户名唯一;
-- 不同公寓或不同 Asterisk 实例可出现同名用户名,但彼此不可见;
-- * 所有读写必须同时带 gyid + instance_code,禁止只按 username 查(跨公寓串号);
-- * auth_id 采用 AUTH-<instance_code>-<username>,在实例内全局唯一,供 ps_auths.id 使用。
-- 口令策略:
-- * secret_mode='PLAIN':库内口令,默认 psw.voip(内网/联调用);
-- * secret_mode='ENV' :只存环境变量名 secret_ref,取值在渲染/同步时注入,
-- 生产建议用这一档(与 APT_AMI_SECRET 同一原则,不把口令写进库与日志);
-- * 两种模式都保留 secret_version / last_rotated_at,便于轮换与审计。
-- 幂等:建表 IF NOT EXISTS;种子与视图可重复执行。
-- =============================================================================
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS apt_sip_auth (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL COMMENT '公寓标识(租户隔离键)',
instance_code VARCHAR(64) NOT NULL COMMENT 'Asterisk 实例(apt_ast_instance.instance_code)',
username VARCHAR(64) NOT NULL COMMENT 'SIP 用户名/分机号',
auth_id VARCHAR(128) NOT NULL COMMENT 'Asterisk ps_auths.id,约定 AUTH-<instance>-<username>',
endpoint_id VARCHAR(64) NULL COMMENT '关联分机(1:1,可空以便先建认证后绑分机)',
auth_type VARCHAR(20) NOT NULL DEFAULT 'userpass' COMMENT 'Asterisk auth_type',
secret_mode VARCHAR(10) NOT NULL DEFAULT 'PLAIN' COMMENT 'PLAIN=库内口令 / ENV=环境变量',
secret_plain VARCHAR(128) NOT NULL DEFAULT 'psw.voip' COMMENT 'secret_mode=PLAIN 时的口令;空串表示未设置',
secret_ref VARCHAR(64) NULL COMMENT 'secret_mode=ENV 时的环境变量名',
secret_version INT NOT NULL DEFAULT 1 COMMENT '口令版本,轮换时 +1',
realm VARCHAR(64) NOT NULL DEFAULT 'asterisk' COMMENT 'Asterisk auth realm',
state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE/DISABLED/ROTATING',
enabled TINYINT NOT NULL DEFAULT 1,
last_rotated_at DATETIME(3) NULL,
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_sipauth_gyid_auth_id (gyid, auth_id),
UNIQUE KEY uk_sipauth_instance_username (gyid, instance_code, username),
UNIQUE KEY uk_sipauth_instance_endpoint (gyid, instance_code, endpoint_id),
KEY idx_sipauth_gyid_state (gyid, instance_code, state, enabled),
KEY idx_sipauth_gyid_endpoint (gyid, endpoint_id),
CONSTRAINT ck_sipauth_secret_mode CHECK (secret_mode IN ('PLAIN','ENV')),
CONSTRAINT ck_sipauth_state CHECK (state IN ('ACTIVE','DISABLED','ROTATING')),
CONSTRAINT ck_sipauth_auth_type CHECK (auth_type IN ('userpass','md5','userpass,md5')),
CONSTRAINT ck_sipauth_secret_ref CHECK (secret_mode = 'PLAIN' OR secret_ref IS NOT NULL)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
COMMENT='SIP 认证账号(按公寓 + Asterisk 实例隔离;默认口令 psw.voip 仅限内网/联调)';
-- -----------------------------------------------------------------------------
-- 种子:为已登记分机补认证账号(每个公寓取第一个启用的实例;已存在则跳过)
-- -----------------------------------------------------------------------------
INSERT INTO apt_sip_auth
(gyid, instance_code, username, auth_id, endpoint_id, secret_plain)
SELECT e.gyid,
i.instance_code,
e.extension,
CONCAT('AUTH-', i.instance_code, '-', e.extension),
e.endpoint_id,
'psw.voip'
FROM apt_sip_endpoint e
JOIN apt_ast_instance i
ON i.id = (SELECT i2.id FROM apt_ast_instance i2
WHERE i2.gyid = e.gyid AND i2.deleted = 0 AND i2.enabled = 1
ORDER BY i2.id LIMIT 1)
LEFT JOIN apt_sip_auth a
ON a.gyid = e.gyid AND a.instance_code = i.instance_code AND a.username = e.extension
WHERE e.deleted = 0
AND a.id IS NULL;
-- -----------------------------------------------------------------------------
-- Asterisk realtime 视图:ps_auths / ps_endpoints(多实例按 gyid + instance_code 过滤)
-- ENV 模式在视图中不回显口令(password 为 NULL),由同步器渲染后再下发。
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW ps_auths AS
SELECT auth_id AS id,
auth_type,
username,
CASE WHEN secret_mode = 'PLAIN' THEN secret_plain END AS password,
realm
FROM apt_sip_auth
WHERE deleted = 0 AND enabled = 1 AND state = 'ACTIVE';
CREATE OR REPLACE VIEW ps_endpoints AS
SELECT e.endpoint_id AS id,
'transport-udp' AS transport,
a.aor_id AS aors,
au.auth_id AS auth,
'from-internal' AS context,
'ulaw' AS allow,
'all' AS disallow,
'no' AS direct_media,
e.extension AS callerid,
'yes' AS rewrite_contact
FROM apt_sip_endpoint e
JOIN apt_sip_aor a
ON a.gyid = e.gyid AND a.endpoint_id = e.endpoint_id AND a.deleted = 0
LEFT JOIN apt_sip_auth au
ON au.gyid = e.gyid AND au.endpoint_id = e.endpoint_id
AND au.deleted = 0 AND au.enabled = 1 AND au.state = 'ACTIVE'
WHERE e.deleted = 0 AND e.enabled = 1;
-- -----------------------------------------------------------------------------
-- 校验
-- -----------------------------------------------------------------------------
SELECT '=== 36. apt_sip_auth 列数(应 20)===' AS section;
SELECT COUNT(*) AS auth_columns
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_sip_auth';
SELECT '=== 37. 唯一键(应 3:auth_id / instance+username / instance+endpoint)===' 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_auth' AND NON_UNIQUE = 0
GROUP BY INDEX_NAME;
SELECT '=== 38. 默认口令 ===' AS section;
SELECT COLUMN_NAME, COLUMN_DEFAULT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_sip_auth'
AND COLUMN_NAME IN ('secret_plain','secret_mode');
SELECT '=== 39. 现有认证账号(按实例分布)===' AS section;
SELECT gyid, instance_code, COUNT(*) AS accounts, SUM(secret_mode = 'PLAIN') AS plain_accounts
FROM apt_sip_auth
WHERE deleted = 0
GROUP BY gyid, instance_code;
@@ -0,0 +1,77 @@
-- =============================================================================
-- 平板刷脸照片(叫乘作业留痕)
-- 定位:平板在「刷脸确认成功(ANSWERED + VERIFIED)」后上传的抓拍原图,
-- 库里只存「存储路径 + 校验信息 + 状态」,图像本体落在文件/对象存储,不进 BLOB。
-- 关联:apt_tablet_call_result (result_id) → gy_jcxx (jcid),一结果最多一张有效照片。
-- 路径规范(相对路径入库,根目录随环境配置 apt.photo.root):
-- face/{gyid}/{yyyy}/{MM}/{dd}/{jcid}/{photoId}.{jpg|png}
-- 例:face/APT-REV01-001/2026/11/12/JC-20261112-093000/PH-3F9A1C2B7D01.jpg
-- 幂等:UNIQUE (gyid, result_id) —— 同一结果重传覆盖同一行(retry_count+1),不产生多张。
-- 失败留痕:upload_state=FAILED + fail_reason,结果表 face_photo_state=FAILED,
-- 不影响 ryzt=3(状态机已在结果受理时完成),平板可原样重试。
-- =============================================================================
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS apt_tablet_face_photo (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
photo_id VARCHAR(64) NOT NULL COMMENT '照片业务键,PH-<12位十六进制>',
jcid VARCHAR(64) NOT NULL COMMENT '叫乘作业号(关联 gy_jcxx.jcid)',
task_id VARCHAR(64) NOT NULL COMMENT '平板任务号',
result_id VARCHAR(64) NOT NULL COMMENT '平板结果号(apt_tablet_call_result.result_id)',
ryid VARCHAR(64) NULL COMMENT '刷脸命中人员(verified_ryid)',
device_code VARCHAR(64) NOT NULL,
storage_target VARCHAR(20) NOT NULL DEFAULT 'LOCAL' COMMENT 'LOCAL / OSS',
storage_path VARCHAR(512) NOT NULL COMMENT '相对路径,不含根目录与域名',
file_name VARCHAR(128) NOT NULL,
content_type VARCHAR(64) NOT NULL COMMENT 'image/jpeg / image/png',
file_size BIGINT NOT NULL DEFAULT 0,
sha256 CHAR(64) NULL COMMENT '落盘后校验和,用于完整性核验',
upload_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/STORED/FAILED/PURGED',
fail_reason VARCHAR(255) NULL,
retry_count INT NOT NULL DEFAULT 0,
captured_at DATETIME(3) NULL COMMENT '平板抓拍时间(occurred_at)',
uploaded_at DATETIME(3) NULL,
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_facephoto_gyid_id (gyid, photo_id),
UNIQUE KEY uk_facephoto_gyid_result (gyid, result_id),
KEY idx_facephoto_gyid_jcid (gyid, jcid, upload_state),
KEY idx_facephoto_gyid_state (gyid, upload_state),
CONSTRAINT ck_facephoto_state CHECK (upload_state IN ('PENDING','STORED','FAILED','PURGED')),
CONSTRAINT ck_facephoto_target CHECK (storage_target IN ('LOCAL','OSS'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
COMMENT='平板刷脸照片(只存路径与校验信息,图像落文件/对象存储)';
-- -----------------------------------------------------------------------------
-- 结果表补指针列:便于按 result 直接回显,不必每次 join
-- -----------------------------------------------------------------------------
CALL apt_add_column_if_missing('apt_tablet_call_result', 'face_photo_id',
"face_photo_id VARCHAR(64) NULL COMMENT '最近一次上传的刷脸照片(apt_tablet_face_photo.photo_id)'");
CALL apt_add_column_if_missing('apt_tablet_call_result', 'face_photo_state',
"face_photo_state VARCHAR(20) NULL COMMENT '照片态:PENDING/STORED/FAILED,未上传为 NULL'");
-- -----------------------------------------------------------------------------
-- 校验
-- -----------------------------------------------------------------------------
SELECT '=== 40. 照片表列数(应 22)===' AS section;
SELECT COUNT(*) AS photo_columns
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_tablet_face_photo';
SELECT '=== 41. 唯一键(应 3:主键 + photo_id + result_id)===' 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_tablet_face_photo' AND NON_UNIQUE = 0
GROUP BY INDEX_NAME;
SELECT '=== 42. 结果表照片指针列(应 2)===' AS section;
SELECT COLUMN_NAME, COLUMN_TYPE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_tablet_call_result'
AND COLUMN_NAME IN ('face_photo_id','face_photo_state');
SELECT '=== 43. 现有照片记录 ===' AS section;
SELECT upload_state, COUNT(*) AS cnt FROM apt_tablet_face_photo WHERE deleted = 0 GROUP BY upload_state;
+41
View File
@@ -0,0 +1,41 @@
-- =============================================================================
-- 刷脸照片保留期(最多 1 个月)+ 定期清理任务登记
-- 清理范围:apt_tablet_face_photo 中 COALESCE(uploaded_at, captured_at, create_time)
-- 早于 now - retentionDays 的记录:先删文件,再物理删记录。
-- 保留天数由 apt.photo.retention-days 控制(默认 30,上限按业务 1 个月)。
-- 任务:aptPhotoPurgeTask.purge(),建议每天 03:30(避开叫乘高峰)。
-- 幂等:sys_job 按 invoke_target 判重,可重复执行。
-- =============================================================================
SET NAMES utf8mb4;
INSERT INTO sys_job
(job_name, job_group, invoke_target, cron_expression,
misfire_policy, concurrent, status, create_by, create_time, remark)
SELECT '刷脸照片过期清理(保留 30 天)',
'SYSTEM',
'aptPhotoPurgeTask.purge()',
'0 30 3 * * ?',
'1',
1,
'1',
'IT-5',
NOW(3),
'每天 03:30 清理超过 apt.photo.retention-days(默认 30 天)的照片文件与记录'
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM sys_job WHERE invoke_target = 'aptPhotoPurgeTask.purge()'
);
-- -----------------------------------------------------------------------------
-- 校验
-- -----------------------------------------------------------------------------
SELECT '=== 44. 清理任务登记(应 1 行,status=1)===' AS section;
SELECT job_id, job_name, invoke_target, cron_expression, status
FROM sys_job
WHERE invoke_target = 'aptPhotoPurgeTask.purge()';
SELECT '=== 45. 当前照片记录与最老时间 ===' AS section;
SELECT COUNT(*) AS photos,
MIN(COALESCE(uploaded_at, captured_at, create_time)) AS oldest_at,
SUM(COALESCE(uploaded_at, captured_at, create_time) < DATE_SUB(NOW(3), INTERVAL 30 DAY)) AS expired
FROM apt_tablet_face_photo;
+183
View File
@@ -0,0 +1,183 @@
-- =============================================================================
-- 平板作业(保洁 / 维修)与作业人员
-- 1) 保洁复用既有 gy_clean_task(房态重算已依赖它:PENDING→3 待清扫 / PROCESSING→6 保洁中),
-- 这里补「执行人、开始时间、备注、来源、房号快照」;
-- 2) 维修新建 apt_repair_order(REPORTED→ASSIGNED→IN_PROGRESS→DONE/CANCELLED),
-- 维修期间房态置 4(不能入住)并在 disabled_reason 标记 REPAIR:<orderId>;
-- 3) 作业人员 apt_staff(保洁/维修,可绑后台账号与平板设备);
-- 4) 状态流水 apt_job_event(两类作业共用,便于追溯与统计)。
-- 5) 权限点挂在既有父菜单 3000(公寓端)下,menu_type=F。
-- 幂等:列/表/权限均做存在性判断,可重复执行。
-- =============================================================================
SET NAMES utf8mb4;
-- -----------------------------------------------------------------------------
-- 1. 保洁任务补列(沿用 gy_clean_task)
-- -----------------------------------------------------------------------------
CALL apt_add_column_if_missing('gy_clean_task', 'assignee_staff_id',
"assignee_staff_id VARCHAR(64) NULL COMMENT '保洁员(apt_staff.staff_id)'");
CALL apt_add_column_if_missing('gy_clean_task', 'started_at',
"started_at DATETIME(3) NULL COMMENT '开始保洁时间'");
CALL apt_add_column_if_missing('gy_clean_task', 'remark',
"remark VARCHAR(255) NULL COMMENT '平板备注'");
CALL apt_add_column_if_missing('gy_clean_task', 'source',
"source VARCHAR(20) NOT NULL DEFAULT 'TABLET' COMMENT 'TABLET=平板上报 / MANUAL=后台补录'");
CALL apt_add_column_if_missing('gy_clean_task', 'room_no',
"room_no VARCHAR(32) NULL COMMENT '房号快照(便于按房号查询与展示)'");
-- -----------------------------------------------------------------------------
-- 2. 作业人员
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS apt_staff (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
staff_id VARCHAR(64) NOT NULL COMMENT '人员业务键 ST-<12位>',
staff_type VARCHAR(20) NOT NULL COMMENT 'CLEANER / REPAIRMAN / BOTH',
name VARCHAR(64) NOT NULL,
work_no VARCHAR(32) NULL COMMENT '工号(同公寓内唯一,可空)',
phone VARCHAR(32) NULL,
sys_user_id BIGINT NULL COMMENT '绑定的后台账号(sys_user.user_id),用于权限归属',
device_code VARCHAR(64) NULL COMMENT '绑定的平板设备(X-Device-Code)',
duty_state VARCHAR(20) NOT NULL DEFAULT 'IDLE' COMMENT 'IDLE / BUSY / OFFLINE',
state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE / DISABLED',
remark VARCHAR(255) NULL,
biz_version BIGINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
create_by VARCHAR(64) NULL,
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_staff_gyid_id (gyid, staff_id),
UNIQUE KEY uk_staff_gyid_workno (gyid, work_no),
KEY idx_staff_gyid_type (gyid, staff_type, state),
KEY idx_staff_gyid_device (gyid, device_code),
CONSTRAINT ck_staff_type CHECK (staff_type IN ('CLEANER','REPAIRMAN','BOTH')),
CONSTRAINT ck_staff_state CHECK (state IN ('ACTIVE','DISABLED')),
CONSTRAINT ck_staff_duty CHECK (duty_state IN ('IDLE','BUSY','OFFLINE'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
COMMENT='公寓作业人员(保洁/维修)';
-- -----------------------------------------------------------------------------
-- 3. 维修工单
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS apt_repair_order (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
order_id VARCHAR(64) NOT NULL COMMENT '工单号 RO-<12位>',
room_id VARCHAR(64) NOT NULL,
room_no VARCHAR(32) NOT NULL,
category VARCHAR(20) NOT NULL DEFAULT 'OTHER' COMMENT 'ELECTRIC/PLUMBING/DOOR_WINDOW/AIRCON/OTHER',
description VARCHAR(500) NOT NULL,
priority VARCHAR(10) NOT NULL DEFAULT 'NORMAL' COMMENT 'NORMAL / URGENT',
source VARCHAR(20) NOT NULL DEFAULT 'TABLET' COMMENT 'TABLET / MANUAL',
reporter_staff_id VARCHAR(64) NULL,
reporter_name VARCHAR(64) NULL,
assignee_staff_id VARCHAR(64) NULL,
assignee_name VARCHAR(64) NULL,
state VARCHAR(20) NOT NULL DEFAULT 'REPORTED' COMMENT 'REPORTED/ASSIGNED/IN_PROGRESS/DONE/CANCELLED',
room_status_before VARCHAR(2) NULL,
room_status_after VARCHAR(2) NULL,
result VARCHAR(255) NULL COMMENT '维修结果(DONE 必填)',
remark VARCHAR(500) NULL,
reported_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
assigned_at DATETIME(3) NULL,
started_at DATETIME(3) NULL,
finished_at DATETIME(3) NULL,
duration_sec BIGINT NULL COMMENT '作业时长(开始→完成)',
version INT NOT NULL DEFAULT 0,
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_repair_gyid_order (gyid, order_id),
KEY idx_repair_gyid_state (gyid, state, reported_at),
KEY idx_repair_gyid_room (gyid, room_id, state),
KEY idx_repair_gyid_assignee (gyid, assignee_staff_id, state),
CONSTRAINT ck_repair_state CHECK (state IN ('REPORTED','ASSIGNED','IN_PROGRESS','DONE','CANCELLED')),
CONSTRAINT ck_repair_priority CHECK (priority IN ('NORMAL','URGENT')),
CONSTRAINT ck_repair_category CHECK (category IN ('ELECTRIC','PLUMBING','DOOR_WINDOW','AIRCON','OTHER'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
COMMENT='维修工单(维修期间房态 4=不能入住)';
-- -----------------------------------------------------------------------------
-- 4. 作业状态流水(保洁 + 维修共用)
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS apt_job_event (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
event_id VARCHAR(64) NOT NULL,
biz_type VARCHAR(20) NOT NULL COMMENT 'CLEAN / REPAIR',
biz_id VARCHAR(64) NOT NULL COMMENT 'task_id / order_id',
room_id VARCHAR(64) NULL,
from_state VARCHAR(20) NULL,
to_state VARCHAR(20) NOT NULL,
operator_staff VARCHAR(64) NULL,
operator_name VARCHAR(64) NULL,
device_code VARCHAR(64) NULL,
remark VARCHAR(255) NULL,
occurred_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_jobevent_gyid_id (gyid, event_id),
KEY idx_jobevent_gyid_biz (gyid, biz_type, biz_id, occurred_at),
CONSTRAINT ck_jobevent_type CHECK (biz_type IN ('CLEAN','REPAIR'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
COMMENT='作业状态流水(保洁/维修)';
-- -----------------------------------------------------------------------------
-- 5. 权限点(挂在 3000 公寓端下,menu_type=F)
-- -----------------------------------------------------------------------------
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, menu_type, visible, status, perms, create_by, create_time, remark)
SELECT 3230, '作业工单查看', 3000, 30, 'F', '0', '0', 'apt:workorder:list', 'IT-5', NOW(3), '保洁/维修工单列表'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'apt:workorder:list');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, menu_type, visible, status, perms, create_by, create_time, remark)
SELECT 3231, '作业工单详情', 3000, 31, 'F', '0', '0', 'apt:workorder:query', 'IT-5', NOW(3), NULL
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'apt:workorder:query');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, menu_type, visible, status, perms, create_by, create_time, remark)
SELECT 3232, '作业派单', 3000, 32, 'F', '0', '0', 'apt:workorder:assign', 'IT-5', NOW(3), '维修派单/改派'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'apt:workorder:assign');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, menu_type, visible, status, perms, create_by, create_time, remark)
SELECT 3233, '作业处置', 3000, 33, 'F', '0', '0', 'apt:workorder:handle', 'IT-5', NOW(3), '开始/完成/取消'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'apt:workorder:handle');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, menu_type, visible, status, perms, create_by, create_time, remark)
SELECT 3234, '作业统计', 3000, 34, 'F', '0', '0', 'apt:workorder:stats', 'IT-5', NOW(3), '按类型/状态/人员统计'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'apt:workorder:stats');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, menu_type, visible, status, perms, create_by, create_time, remark)
SELECT 3235, '作业人员列表', 3000, 35, 'F', '0', '0', 'apt:staff:list', 'IT-5', NOW(3), '保洁/维修人员'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'apt:staff:list');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, menu_type, visible, status, perms, create_by, create_time, remark)
SELECT 3236, '作业人员详情', 3000, 36, 'F', '0', '0', 'apt:staff:query', 'IT-5', NOW(3), NULL
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'apt:staff:query');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, menu_type, visible, status, perms, create_by, create_time, remark)
SELECT 3237, '作业人员新增', 3000, 37, 'F', '0', '0', 'apt:staff:add', 'IT-5', NOW(3), NULL
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'apt:staff:add');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, menu_type, visible, status, perms, create_by, create_time, remark)
SELECT 3238, '作业人员编辑', 3000, 38, 'F', '0', '0', 'apt:staff:edit', 'IT-5', NOW(3), NULL
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'apt:staff:edit');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, menu_type, visible, status, perms, create_by, create_time, remark)
SELECT 3239, '作业人员删除', 3000, 39, 'F', '0', '0', 'apt:staff:remove', 'IT-5', NOW(3), NULL
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'apt:staff:remove');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, menu_type, visible, status, perms, create_by, create_time, remark)
SELECT 3240, '作业人员启停', 3000, 40, 'F', '0', '0', 'apt:staff:changeStatus', 'IT-5', NOW(3), NULL
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'apt:staff:changeStatus');
-- -----------------------------------------------------------------------------
-- 6. 校验
-- -----------------------------------------------------------------------------
SELECT '=== 46. 作业相关表(应 3)===' AS section;
SELECT COUNT(*) AS tables_cnt
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME IN ('apt_staff','apt_repair_order','apt_job_event');
SELECT '=== 47. 保洁任务补列(应 5)===' AS section;
SELECT COUNT(*) AS clean_columns
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'gy_clean_task'
AND COLUMN_NAME IN ('assignee_staff_id','started_at','remark','source','room_no');
SELECT '=== 48. 新增权限点(应 11)===' AS section;
SELECT COUNT(*) AS perms_cnt FROM sys_menu
WHERE perms IN ('apt:workorder:list','apt:workorder:query','apt:workorder:assign',
'apt:workorder:handle','apt:workorder:stats',
'apt:staff:list','apt:staff:query','apt:staff:add','apt:staff:edit',
'apt:staff:remove','apt:staff:changeStatus');
+60
View File
@@ -0,0 +1,60 @@
-- =============================================================================
-- 作业工单 / 作业人员前端页面菜单(menu_type='C',可重放)
-- 前提:26_jobs_staff.sql 已建 F 权限点 3230~3240(apt:workorder:* / apt:staff:*)。
-- 口径(与 13_it2_frontend_menus.sql 一致):
-- 1) 只新增 C 类型页面菜单,不改 3000 父目录、不改任何 F 权限点;
-- 2) 路由由后端 getRouters 下发,component 走 loadView('@/views/' + component);
-- 3) C 菜单 perms 留空:可见性由 sys_role_menu 控制,按钮级权限由 F 权限点控制。
-- 组件路径(须与前端一致):
-- src/views/apt/workorder/index.vue ← 3204 作业工单
-- src/views/apt/staff/index.vue ← 3205 作业人员
-- =============================================================================
SET NAMES utf8mb4;
-- -----------------------------------------------------------------------------
-- 1. 页面菜单(C):父目录 3000(path='apt'),完整路由 /apt/workorder、/apt/staff
-- -----------------------------------------------------------------------------
INSERT INTO sys_menu
(menu_id, menu_name, parent_id, order_num, path, component, query, route_name,
is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES
(3204, '作业工单', 3000, 8, 'workorder', 'apt/workorder/index', '', '', 1, 0, 'C', '0', '0',
'', 'build', 'IT-5', NOW(3),
'保洁/维修工单:列表、详情、维修派单、取消、统计;状态推进由平板执行'),
(3205, '作业人员', 3000, 9, 'staff', 'apt/staff/index', '', '', 1, 0, 'C', '0', '0',
'', 'peoples', 'IT-5', NOW(3),
'保洁/维修人员:新增、编辑、启停、软删;device_code 绑定平板')
ON DUPLICATE KEY UPDATE menu_name = VALUES(menu_name), parent_id = VALUES(parent_id),
order_num = VALUES(order_num), path = VALUES(path), component = VALUES(component),
menu_type = VALUES(menu_type), visible = VALUES(visible), status = VALUES(status),
icon = VALUES(icon), remark = VALUES(remark);
-- -----------------------------------------------------------------------------
-- 2. 关联超管角色(超管本身已有 *:*:*,此处保证 getRouters 下发并留痕)
-- -----------------------------------------------------------------------------
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, m.menu_id FROM sys_menu m
WHERE m.menu_id IN (3204, 3205)
AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
-- -----------------------------------------------------------------------------
-- 3. 校验
-- -----------------------------------------------------------------------------
SELECT '=== 49. 作业页面菜单(应 2 条 C)===' AS section;
SELECT menu_id, menu_name, parent_id, path, component, menu_type, visible, status, icon
FROM sys_menu WHERE menu_id IN (3204, 3205) ORDER BY menu_id;
SELECT '=== 50. 组件路径(须与前端 src/views/apt/** 一致)===' AS section;
SELECT menu_id, CONCAT('src/views/', component, '.vue') AS vue_file
FROM sys_menu WHERE menu_id IN (3204, 3205) ORDER BY menu_id;
SELECT '=== 51. C 菜单已关联超管(应 2)===' AS section;
SELECT COUNT(*) AS admin_menu_bound
FROM sys_role_menu rm JOIN sys_menu m ON m.menu_id = rm.menu_id
WHERE rm.role_id = 1 AND m.menu_id IN (3204, 3205) AND m.menu_type = 'C';
SELECT '=== 52. 公寓端菜单树构成(应 M=1 / C=9 / F=38)===' AS section;
SELECT menu_type, COUNT(*) AS cnt
FROM sys_menu
WHERE menu_id = 3000 OR parent_id = 3000
GROUP BY menu_type ORDER BY menu_type;
+87
View File
@@ -0,0 +1,87 @@
-- =============================================================================
-- 保洁自动建单 / 报修自动流转(增量,可重放)
-- 口径:
-- 1) 离寓判定以 gy_jcxx 在寓事实为准:finish_ly='0' AND sjlysj IS NULL;
-- gy_rooms.in_person_count 由该口径重算,不再依赖 +1/-1。
-- 2) 同房同一时刻只允许一条活跃保洁任务(PENDING/PROCESSING):
-- active_room 生成列 + 唯一索引,服务层先判后插,库层兜底并发。
-- 3) source 增加 AUTO(全员离寓自动建单);TABLET=平板上报 / MANUAL=后台补录。
-- 幂等:列/索引均做 information_schema 判断,可重复执行。
-- =============================================================================
SET NAMES utf8mb4;
-- -----------------------------------------------------------------------------
-- 1. 保洁任务:活跃占位列 + 唯一索引(同房仅一条待处理/保洁中)
-- -----------------------------------------------------------------------------
CALL apt_add_column_if_missing('gy_clean_task', 'active_room',
"active_room VARCHAR(64) GENERATED ALWAYS AS (CASE WHEN state IN ('PENDING','PROCESSING') AND deleted = 0 THEN room_id END) STORED COMMENT '活跃任务房号占位:同房同时只允许一条待处理/保洁中'");
SET @apt_idx_active = (SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'gy_clean_task'
AND INDEX_NAME = 'uk_cleantask_gyid_active_room');
SET @apt_sql_active = IF(@apt_idx_active = 0,
'ALTER TABLE gy_clean_task ADD UNIQUE KEY uk_cleantask_gyid_active_room (gyid, active_room)',
'SELECT 1');
PREPARE stmt_cleantask_active FROM @apt_sql_active;
EXECUTE stmt_cleantask_active;
DEALLOCATE PREPARE stmt_cleantask_active;
-- -----------------------------------------------------------------------------
-- 2. 查询索引:按房号找活跃任务 / 按房号统计在寓人数
-- -----------------------------------------------------------------------------
SET @apt_idx_room = (SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'gy_clean_task'
AND INDEX_NAME = 'idx_cleantask_gyid_room_state');
SET @apt_sql_room = IF(@apt_idx_room = 0,
'ALTER TABLE gy_clean_task ADD KEY idx_cleantask_gyid_room_state (gyid, room_id, state, deleted)',
'SELECT 1');
PREPARE stmt_cleantask_room FROM @apt_sql_room;
EXECUTE stmt_cleantask_room;
DEALLOCATE PREPARE stmt_cleantask_room;
SET @apt_idx_stay = (SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'gy_jcxx'
AND INDEX_NAME = 'idx_jcxx_gyid_room_leave');
SET @apt_sql_stay = IF(@apt_idx_stay = 0,
'ALTER TABLE gy_jcxx ADD KEY idx_jcxx_gyid_room_leave (gyid, fjh, finish_ly)',
'SELECT 1');
PREPARE stmt_jcxx_room FROM @apt_sql_stay;
EXECUTE stmt_jcxx_room;
DEALLOCATE PREPARE stmt_jcxx_room;
-- -----------------------------------------------------------------------------
-- 3. source 取值口径补充 AUTO
-- -----------------------------------------------------------------------------
ALTER TABLE gy_clean_task
MODIFY COLUMN source VARCHAR(20) NOT NULL DEFAULT 'TABLET'
COMMENT 'TABLET=平板上报 / MANUAL=后台补录 / AUTO=全员离寓自动建单';
-- -----------------------------------------------------------------------------
-- 4. 校验
-- -----------------------------------------------------------------------------
SELECT '=== 53. active_room 生成列(应 1:STORED GENERATED)===' AS section;
SELECT COLUMN_NAME, COLUMN_TYPE, EXTRA
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'gy_clean_task'
AND COLUMN_NAME = 'active_room';
SELECT '=== 54. 同房唯一活跃任务索引(应 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 = 'gy_clean_task'
AND INDEX_NAME IN ('uk_cleantask_gyid_active_room','idx_cleantask_gyid_room_state')
GROUP BY INDEX_NAME;
SELECT '=== 55. 在寓判定索引(应 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 = 'gy_jcxx'
AND INDEX_NAME = 'idx_jcxx_gyid_room_leave'
GROUP BY INDEX_NAME;
SELECT '=== 56. 当前活跃保洁任务(每个房间应 <= 1)===' AS section;
SELECT gyid, room_id, COUNT(*) AS active_cnt
FROM gy_clean_task
WHERE state IN ('PENDING','PROCESSING') AND deleted = 0
GROUP BY gyid, room_id
HAVING active_cnt > 1;
+91
View File
@@ -0,0 +1,91 @@
-- =============================================================================
-- IT-6 权限点与查询索引(增量,可重放)
-- 范围:上报续传(outbox/水位)、M4 人员关联、M5 设备指令入队
-- 口径:
-- 1) 只新增 F 权限点,不新增页面菜单(duty 入口与 M4/M5 页面不在本轮交付);
-- 2) 权限点挂在 3000 公寓端目录下,与既有 3230~3240 作业权限并列;
-- 3) 索引只补查询路径,不改任何业务列。
-- =============================================================================
SET NAMES utf8mb4;
-- -----------------------------------------------------------------------------
-- 1. 权限点(F)
-- -----------------------------------------------------------------------------
INSERT INTO sys_menu
(menu_id, menu_name, parent_id, order_num, path, component, query, route_name,
is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES
(3250, '上报队列查询', 3000, 30, '', '', '', '', 1, 0, 'F', '0', '0',
'apt:report:list', '#', 'IT-6', NOW(3), 'outbox 列表:bizType/state/时间'),
(3251, '上报统计查询', 3000, 31, '', '', '', '', 1, 0, 'F', '0', '0',
'apt:report:query', '#', 'IT-6', NOW(3), '队列汇总 + 水位 + 心跳偏差'),
(3252, '上报重试', 3000, 32, '', '', '', '', 1, 0, 'F', '0', '0',
'apt:report:retry', '#', 'IT-6', NOW(3), 'FAILED/DEAD 重新入队,复用原幂等键'),
(3253, '人员列表', 3000, 33, '', '', '', '', 1, 0, 'F', '0', '0',
'apt:person:list', '#', 'IT-6', NOW(3), 'M4 人员列表'),
(3254, '人员详情', 3000, 34, '', '', '', '', 1, 0, 'F', '0', '0',
'apt:person:query', '#', 'IT-6', NOW(3), 'M4 人员详情与关联 Tab'),
(3255, '人员修改(拒绝)', 3000, 35, '', '', '', '', 1, 0, 'F', '0', '0',
'apt:person:edit', '#', 'IT-6', NOW(3), '快照只读,写入口固定返回 40904'),
(3256, '设备列表', 3000, 36, '', '', '', '', 1, 0, 'F', '0', '0',
'apt:device:list', '#', 'IT-6', NOW(3), 'M5 设备档案与在线态'),
(3257, '设备详情', 3000, 37, '', '', '', '', 1, 0, 'F', '0', '0',
'apt:device:query', '#', 'IT-6', NOW(3), 'M5 设备详情与指令记录'),
(3258, '设备指令入队', 3000, 38, '', '', '', '', 1, 0, 'F', '0', '0',
'apt:device:enqueue', '#', 'IT-6', NOW(3), '只入队 PENDING,不伪造 ACKED')
ON DUPLICATE KEY UPDATE menu_name = VALUES(menu_name), parent_id = VALUES(parent_id),
order_num = VALUES(order_num), menu_type = VALUES(menu_type), status = VALUES(status),
perms = VALUES(perms), remark = VALUES(remark);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, m.menu_id FROM sys_menu m
WHERE m.menu_id BETWEEN 3250 AND 3258
AND m.menu_type = 'F'
AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
-- -----------------------------------------------------------------------------
-- 2. 查询索引
-- -----------------------------------------------------------------------------
SET @apt_idx_outbox_time = (SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_report_outbox'
AND INDEX_NAME = 'idx_outbox_gyid_occurred');
SET @apt_sql_outbox_time = IF(@apt_idx_outbox_time = 0,
'ALTER TABLE apt_report_outbox ADD KEY idx_outbox_gyid_occurred (gyid, occurred_at)',
'SELECT 1');
PREPARE stmt_outbox_time FROM @apt_sql_outbox_time;
EXECUTE stmt_outbox_time;
DEALLOCATE PREPARE stmt_outbox_time;
SET @apt_idx_cmd_device = (SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_device_command'
AND INDEX_NAME = 'idx_devcmd_gyid_device_state');
SET @apt_sql_cmd_device = IF(@apt_idx_cmd_device = 0,
'ALTER TABLE apt_device_command ADD KEY idx_devcmd_gyid_device_state (gyid, device_code, state, create_time)',
'SELECT 1');
PREPARE stmt_cmd_device FROM @apt_sql_cmd_device;
EXECUTE stmt_cmd_device;
DEALLOCATE PREPARE stmt_cmd_device;
-- -----------------------------------------------------------------------------
-- 3. 校验
-- -----------------------------------------------------------------------------
SELECT '=== 57. IT-6 权限点(应 9)===' AS section;
SELECT menu_id, perms, menu_name FROM sys_menu
WHERE menu_id BETWEEN 3250 AND 3258 ORDER BY menu_id;
SELECT '=== 58. 超管已绑定(应 9)===' AS section;
SELECT COUNT(*) AS admin_bound
FROM sys_role_menu rm JOIN sys_menu m ON m.menu_id = rm.menu_id
WHERE rm.role_id = 1 AND m.menu_id BETWEEN 3250 AND 3258 AND m.menu_type = 'F';
SELECT '=== 59. IT-6 索引(应 2)===' AS section;
SELECT TABLE_NAME, INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS cols
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND INDEX_NAME IN ('idx_outbox_gyid_occurred', 'idx_devcmd_gyid_device_state')
GROUP BY TABLE_NAME, INDEX_NAME;
SELECT '=== 60. outbox / 水位 / 指令表可读(应各 1 行)===' AS section;
SELECT 'outbox' AS t, COUNT(*) AS cnt FROM apt_report_outbox
UNION ALL SELECT 'watermark', COUNT(*) FROM apt_report_watermark
UNION ALL SELECT 'device_command', COUNT(*) FROM apt_device_command;