407 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 数据库迁移 — 自动建表
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) NOT NULL UNIQUE COMMENT '平台抽象ID,如10-00000000',
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;
"#;
// 逐条执行,避免多语句执行问题
for statement in sql.split(';').map(|s| s.trim()).filter(|s| !s.is_empty()) {
sqlx::query(statement).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(())
}