//! 数据库迁移 — 自动建表 use sqlx::MySqlPool; /// 执行数据库迁移,创建所有必要的数据表 pub async fn run(pool: &MySqlPool) -> Result<(), sqlx::Error> { let sql = r#" CREATE TABLE IF NOT EXISTS organizations ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL COMMENT '组织名称', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS projects ( id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, name VARCHAR(100) NOT NULL COMMENT '项目名称', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (organization_id) REFERENCES organizations(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS cabinets ( id BIGINT AUTO_INCREMENT PRIMARY KEY, project_id BIGINT, abstract_id VARCHAR(20) NULL UNIQUE COMMENT '平台抽象ID,如10-00000001', imei VARCHAR(15) NOT NULL UNIQUE COMMENT '4G模块IMEI', auth_str VARCHAR(8) COMMENT '签名安全码', name VARCHAR(100) COMMENT '柜子名称', status TINYINT DEFAULT 0 COMMENT '0离线 1在线', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES projects(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS cabin_boards ( id BIGINT AUTO_INCREMENT PRIMARY KEY, cabinet_id BIGINT NOT NULL, board_id TINYINT NOT NULL COMMENT '仓控板ID 1-6', status TINYINT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (cabinet_id) REFERENCES cabinets(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS compartments ( id BIGINT AUTO_INCREMENT PRIMARY KEY, cabin_board_id BIGINT NOT NULL, channel_id TINYINT NOT NULL COMMENT '通道ID 1-6', status TINYINT DEFAULT 0 COMMENT '0空闲 1充电中 2故障 3禁用', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (cabin_board_id) REFERENCES cabin_boards(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, phone VARCHAR(11) NOT NULL UNIQUE COMMENT '手机号', name VARCHAR(50) COMMENT '姓名', password VARCHAR(255) NOT NULL, role TINYINT DEFAULT 0 COMMENT '0普通 1企业管理员 2总管理员', organization_id BIGINT, status TINYINT DEFAULT 1 COMMENT '0禁用 1启用', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (organization_id) REFERENCES organizations(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS charge_records ( id BIGINT AUTO_INCREMENT PRIMARY KEY, compartment_id BIGINT NOT NULL, cabinet_id BIGINT NOT NULL, start_time TIMESTAMP NULL COMMENT '充电开始时间', end_time TIMESTAMP NULL COMMENT '充电结束时间', energy DECIMAL(10,2) COMMENT '充电量kWh', max_temperature DECIMAL(5,1) COMMENT '最高温度', status TINYINT DEFAULT 0 COMMENT '0正常 1异常 2手动停止', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (compartment_id) REFERENCES compartments(id), FOREIGN KEY (cabinet_id) REFERENCES cabinets(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS device_logs ( id BIGINT AUTO_INCREMENT PRIMARY KEY, cabinet_id BIGINT NOT NULL, log_type TINYINT NOT NULL COMMENT '1状态上报 2故障 3告警 4停电', content TEXT COMMENT '日志内容JSON', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (cabinet_id) REFERENCES cabinets(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS energy_stats ( id BIGINT AUTO_INCREMENT PRIMARY KEY, cabinet_id BIGINT NOT NULL, stat_date DATE NOT NULL, total_energy DECIMAL(10,2) COMMENT '总能耗kWh', charge_count INT DEFAULT 0 COMMENT '充电次数', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (cabinet_id) REFERENCES cabinets(id), UNIQUE KEY uk_cabinet_date (cabinet_id, stat_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS operation_logs ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, action VARCHAR(100) NOT NULL COMMENT '操作动作', target_type VARCHAR(50) COMMENT '操作对象类型', target_id BIGINT COMMENT '操作对象ID', detail TEXT COMMENT '操作详情', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS roles ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE COMMENT '角色名称', description VARCHAR(200) COMMENT '角色描述', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS permissions ( id BIGINT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(50) NOT NULL UNIQUE COMMENT '权限码,如device:view', name VARCHAR(100) NOT NULL COMMENT '权限名称', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS role_permissions ( role_id BIGINT NOT NULL, permission_id BIGINT NOT NULL, PRIMARY KEY (role_id, permission_id), FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS user_roles ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS user_scopes ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, scope_type TINYINT NOT NULL COMMENT '1组织 2项目 3设备', scope_id BIGINT NOT NULL COMMENT '对应组织/项目/设备ID', permission_type TINYINT NOT NULL COMMENT '1查看 2操作', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS download_tasks ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, task_type VARCHAR(50) NOT NULL COMMENT 'charge_records/energy_stats/device_logs', status TINYINT DEFAULT 0 COMMENT '0排队 1处理中 2完成 3失败', file_path VARCHAR(500) COMMENT '文件路径', file_name VARCHAR(200) COMMENT '文件名', progress INT DEFAULT 0 COMMENT '进度百分比', error_message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, completed_at TIMESTAMP NULL COMMENT '完成时间', FOREIGN KEY (user_id) REFERENCES users(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS abstract_bindings ( id BIGINT AUTO_INCREMENT PRIMARY KEY, abstract_id VARCHAR(20) NOT NULL UNIQUE COMMENT '平台抽象ID,如10-00000001', cabinet_id BIGINT NOT NULL UNIQUE COMMENT '柜子ID', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (cabinet_id) REFERENCES cabinets(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; "#; // 逐条执行,避免多语句执行问题 for statement in sql.split(';').map(|s| s.trim()).filter(|s| !s.is_empty()) { sqlx::query(statement).execute(pool).await?; } // 兼容已有数据库:将 abstract_id 改为可空 let _ = sqlx::query(r#"ALTER TABLE cabinets MODIFY abstract_id VARCHAR(20) NULL"#) .execute(pool).await; // 创建关键索引(IF NOT EXISTS 避免重复) create_indexes(pool).await?; // 初始化默认权限码(INSERT IGNORE 避免重复插入) seed_permissions(pool).await?; // 初始化默认角色 seed_roles(pool).await?; tracing::info!("数据库迁移完成"); Ok(()) } /// 创建关键查询索引(幂等,已存在则跳过) async fn create_indexes(pool: &MySqlPool) -> Result<(), sqlx::Error> { /// 辅助:检查索引是否存在,不存在则创建 async fn ensure_index( pool: &MySqlPool, table: &str, index: &str, columns: &str, ) -> Result<(), sqlx::Error> { let exists: (i64,) = sqlx::query_as( "SELECT COUNT(*) FROM information_schema.statistics \ WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ?", ) .bind(table) .bind(index) .fetch_one(pool) .await?; if exists.0 == 0 { let ddl = format!("CREATE INDEX {} ON {}({})", index, table, columns); sqlx::query(&ddl).execute(pool).await?; } Ok(()) } ensure_index(pool, "cabinets", "idx_cabinets_project", "project_id").await?; ensure_index(pool, "cabin_boards", "idx_cabin_boards_cabinet", "cabinet_id").await?; ensure_index(pool, "compartments", "idx_compartments_board", "cabin_board_id").await?; ensure_index(pool, "charge_records", "idx_charge_records_cabinet", "cabinet_id").await?; ensure_index(pool, "device_logs", "idx_device_logs_cabinet", "cabinet_id").await?; tracing::info!("数据库索引创建完成"); Ok(()) } /// 插入默认权限码 async fn seed_permissions(pool: &MySqlPool) -> Result<(), sqlx::Error> { let perms: Vec<(&str, &str)> = vec![ ("device:view", "查看设备"), ("device:operate", "操作设备"), ("device:create", "添加设备"), ("device:edit", "编辑设备"), ("device:delete", "删除设备"), ("charge:start", "开始充电"), ("charge:stop", "停止充电"), ("charge:open_door", "开门"), ("charge_record:view", "查看充电记录"), ("charge_record:export", "导出充电记录"), ("device_log:view", "查看设备日志"), ("energy:view", "查看能耗"), ("energy:export", "导出能耗"), ("user:view", "查看用户"), ("user:create", "创建用户"), ("user:edit", "编辑用户"), ("user:delete", "删除用户"), ("role:view", "查看角色"), ("role:create", "创建角色"), ("role:edit", "编辑角色"), ("role:delete", "删除角色"), ("operation_log:view", "查看操作日志"), // 组织管理 ("org:view", "查看组织"), ("org:create", "创建组织"), ("org:edit", "编辑组织"), ("org:delete", "删除组织"), // 项目管理 ("project:view", "查看项目"), ("project:create", "创建项目"), ("project:edit", "编辑项目"), ("project:delete", "删除项目"), ]; for (code, name) in perms { sqlx::query( "INSERT IGNORE INTO permissions (code, name) VALUES (?, ?)" ) .bind(code) .bind(name) .execute(pool) .await?; } tracing::info!("默认权限码初始化完成"); Ok(()) } /// 插入默认角色并分配权限 async fn seed_roles(pool: &MySqlPool) -> Result<(), sqlx::Error> { // 创建总管理员角色 sqlx::query( "INSERT IGNORE INTO roles (name, description) VALUES (?, ?)" ) .bind("super_admin") .bind("总管理员 — 全部权限") .execute(pool) .await?; // 创建企业管理员角色 sqlx::query( "INSERT IGNORE INTO roles (name, description) VALUES (?, ?)" ) .bind("org_admin") .bind("企业管理员 — 管理本组织下项目/设备/人员") .execute(pool) .await?; // 创建普通用户角色 sqlx::query( "INSERT IGNORE INTO roles (name, description) VALUES (?, ?)" ) .bind("normal_user") .bind("普通用户 — 查看/操作指定范围设备") .execute(pool) .await?; // 为总管理员分配所有权限 let super_admin_id: Option<(i64,)> = sqlx::query_as( "SELECT id FROM roles WHERE name = 'super_admin'" ) .fetch_optional(pool) .await?; if let Some((role_id,)) = super_admin_id { // 查询所有权限ID let perm_ids: Vec<(i64,)> = sqlx::query_as( "SELECT id FROM permissions" ) .fetch_all(pool) .await?; for (perm_id,) in perm_ids { sqlx::query( "INSERT IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)" ) .bind(role_id) .bind(perm_id) .execute(pool) .await?; } } // 为企业管理员分配非系统管理权限 let org_admin_id: Option<(i64,)> = sqlx::query_as( "SELECT id FROM roles WHERE name = 'org_admin'" ) .fetch_optional(pool) .await?; let org_perm_codes = [ "device:view", "device:operate", "device:create", "device:edit", "charge:start", "charge:stop", "charge:open_door", "charge_record:view", "charge_record:export", "device_log:view", "energy:view", "energy:export", "user:view", "user:edit", "operation_log:view", // 组织管理 "org:view", "org:create", "org:edit", "org:delete", // 项目管理 "project:view", "project:create", "project:edit", "project:delete", ]; if let Some((role_id,)) = org_admin_id { for code in &org_perm_codes { let perm: Option<(i64,)> = sqlx::query_as( "SELECT id FROM permissions WHERE code = ?" ) .bind(code) .fetch_optional(pool) .await?; if let Some((perm_id,)) = perm { sqlx::query( "INSERT IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)" ) .bind(role_id) .bind(perm_id) .execute(pool) .await?; } } } // 为普通用户分配查看和操作权限 let normal_id: Option<(i64,)> = sqlx::query_as( "SELECT id FROM roles WHERE name = 'normal_user'" ) .fetch_optional(pool) .await?; let normal_perm_codes = [ "device:view", "device:operate", "charge:start", "charge:stop", "charge:open_door", "charge_record:view", "device_log:view", "energy:view", ]; if let Some((role_id,)) = normal_id { for code in &normal_perm_codes { let perm: Option<(i64,)> = sqlx::query_as( "SELECT id FROM permissions WHERE code = ?" ) .bind(code) .fetch_optional(pool) .await?; if let Some((perm_id,)) = perm { sqlx::query( "INSERT IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)" ) .bind(role_id) .bind(perm_id) .execute(pool) .await?; } } } tracing::info!("默认角色及权限分配完成"); Ok(()) }