"""数据库模型 — 连接池管理与表结构初始化""" import logging import aiomysql from src.config import ( MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB, MYSQL_POOL_MIN, MYSQL_POOL_MAX, ) logger = logging.getLogger(__name__) _pool: aiomysql.Pool | None = None async def init_pool() -> aiomysql.Pool: """初始化 MySQL 连接池并建表""" global _pool _pool = await aiomysql.create_pool( host=MYSQL_HOST, port=MYSQL_PORT, user=MYSQL_USER, password=MYSQL_PASSWORD, db=MYSQL_DB, minsize=MYSQL_POOL_MIN, maxsize=MYSQL_POOL_MAX, autocommit=True, ) await _create_tables(_pool) logger.info("MySQL 连接池已初始化") return _pool async def get_pool() -> aiomysql.Pool: """获取连接池""" assert _pool is not None, "数据库连接池未初始化" return _pool async def close_pool(): """关闭连接池""" global _pool if _pool: _pool.close() await _pool.wait_closed() _pool = None # ─── DDL ─────────────────────────────────────────────────────────── async def _create_tables(pool: aiomysql.Pool): async with pool.acquire() as conn: async with conn.cursor() as cur: # 1. 联网终端信息表 await cur.execute(""" CREATE TABLE IF NOT EXISTS `dnt_info` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `serial` VARCHAR(45) UNIQUE NOT NULL COMMENT '设备唯一编码 Device_id', `name` VARCHAR(45) DEFAULT '', `ip` VARCHAR(45) DEFAULT '', `port` INT DEFAULT 0, `mac` VARCHAR(45) DEFAULT '', `subnet` VARCHAR(45) DEFAULT '', `gateway` VARCHAR(45) DEFAULT '', `msgport` INT DEFAULT 0, `version` VARCHAR(45) DEFAULT '', `dtype` VARCHAR(5) DEFAULT '30' COMMENT '设备类型', `poll_duration` INT DEFAULT 0, `reset_duration` INT DEFAULT 0, `state` TINYINT DEFAULT 0 COMMENT '0 offline, 1 online', `last_login` DATETIME NULL, `last_off` DATETIME NULL, `online_total` INT DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """) # 2. 车检器测试参数信息表 await cur.execute(""" CREATE TABLE IF NOT EXISTS `tb_loop_test_info` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(45) DEFAULT '', `dev_model` VARCHAR(24) DEFAULT '', `model_code` TINYINT DEFAULT 0, `hard_ver` VARCHAR(10) DEFAULT '', `soft_ver` VARCHAR(10) DEFAULT '', `relay_exist` TINYINT DEFAULT 0, `relay_pluse` TINYINT DEFAULT 0, `sens_min` INT DEFAULT 0, `sens_max` INT DEFAULT 0, `freq_min` INT DEFAULT 0, `freq_max` INT DEFAULT 0, `peak_min` INT DEFAULT 0, `peak_max` INT DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """) # 3. 设备测试状态表 await cur.execute(""" CREATE TABLE IF NOT EXISTS `tb_state_tst` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `dnt_id` INT NOT NULL COMMENT 'FK → dnt_info.id', `dpg430_addr` TINYINT DEFAULT 0, `pcnum` VARCHAR(10) DEFAULT '' COMMENT '批次号', `serialnum` INT DEFAULT 0 COMMENT '流水号', `sub_type` TINYINT DEFAULT 0 COMMENT '1 DLD110, 2 PD132', `str_type` VARCHAR(30) DEFAULT '', `iffinish` VARCHAR(5) DEFAULT '' COMMENT '是否完成', `fault_info` VARCHAR(100) DEFAULT '', `relay_out` VARCHAR(24) DEFAULT '', `ppvalue` FLOAT DEFAULT 0 COMMENT '峰峰值', `idle_freq` FLOAT DEFAULT 0 COMMENT '开始工作频率', `enter_freq` FLOAT DEFAULT 0, `exit_freq` FLOAT DEFAULT 0, `enter_dist` INT DEFAULT 0, `exit_dist` INT DEFAULT 0, `enter_speed` INT DEFAULT 0, `exit_speed` INT DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX `idx_dnt_id` (`dnt_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """) # 4. 采集表模板(不直接创建表,设备注册时按此结构动态建表) logger.info("数据库表初始化完成") # ─── 设备采集表 CRUD ──────────────────────────────────────────────── COLLECT_TABLE_PREFIX = "tb_collect_" def collect_table_name(device_id: str) -> str: """根据 Device_id 生成采集表名""" return f"{COLLECT_TABLE_PREFIX}{device_id}" async def ensure_collect_table(device_id: str): """确保设备采集表存在""" table = collect_table_name(device_id) pool = await get_pool() async with pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute(f""" CREATE TABLE IF NOT EXISTS `{table}` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `dat_type` TINYINT DEFAULT 0 COMMENT '0心跳 1流量 2探头 3其他 4时间戳 7 RS485 8串口上报 9配置返回 11异常', `raw_data` VARCHAR(380) DEFAULT '', `state` TINYINT DEFAULT 0 COMMENT '0 未处理, 1 已处理', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """) logger.info(f"设备采集表 {table} 已就绪") async def insert_collect_data(device_id: str, dat_type: int, raw_data: str): """向设备采集表插入原始数据""" table = collect_table_name(device_id) pool = await get_pool() async with pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute( f"INSERT INTO `{table}` (dat_type, raw_data) VALUES (%s, %s)", (dat_type, raw_data), ) async def fetch_unparsed(device_id: str) -> list[dict]: """获取未处理的记录 (state=0, dat_type=8)""" table = collect_table_name(device_id) pool = await get_pool() async with pool.acquire() as conn: async with conn.cursor(aiomysql.DictCursor) as cur: await cur.execute( f"SELECT id, raw_data FROM `{table}` WHERE state = 0 AND dat_type = 8 LIMIT 100" ) return await cur.fetchall() async def mark_parsed(device_id: str, record_id: int): """标记记录为已处理""" table = collect_table_name(device_id) pool = await get_pool() async with pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute( f"UPDATE `{table}` SET state = 1 WHERE id = %s", (record_id,) ) # ─── dnt_info CRUD ───────────────────────────────────────────────── async def get_dnt_by_serial(serial: str) -> dict | None: """根据 Device_id (serial) 查询终端""" pool = await get_pool() async with pool.acquire() as conn: async with conn.cursor(aiomysql.DictCursor) as cur: await cur.execute("SELECT * FROM dnt_info WHERE serial = %s", (serial,)) return await cur.fetchone() async def upsert_dnt(serial: str, ip: str, port: int, mac: str, subnet: str, gateway: str, msgport: int, version: str, dtype: str = "30") -> int: """插入或更新终端信息,返回 dnt_info.id""" existing = await get_dnt_by_serial(serial) pool = await get_pool() async with pool.acquire() as conn: async with conn.cursor() as cur: if existing: # 已有记录:更新 IP / 网关 / 上线时间 if (existing["ip"] != ip or existing["gateway"] != gateway or existing["port"] != port or existing["subnet"] != subnet): await cur.execute( """UPDATE dnt_info SET ip=%s, port=%s, subnet=%s, gateway=%s, mac=%s, msgport=%s, version=%s, last_login=NOW(), state=1 WHERE serial=%s""", (ip, port, subnet, gateway, mac, msgport, version, serial), ) else: await cur.execute( "UPDATE dnt_info SET last_login=NOW(), state=1 WHERE serial=%s", (serial,), ) return existing["id"] else: await cur.execute( """INSERT INTO dnt_info (serial, ip, port, mac, subnet, gateway, msgport, version, dtype, last_login, state) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s, NOW(), 1)""", (serial, ip, port, mac, subnet, gateway, msgport, version, dtype), ) return cur.lastrowid async def insert_test_result(dnt_id: int, dpg430_addr: int, pcnum: str, serialnum: int, sub_type: int, str_type: str, iffinish: str, fault_info: str, relay_out: str, ppvalue: float, idle_freq: float, enter_freq: float, exit_freq: float, enter_dist: int, exit_dist: int, enter_speed: int, exit_speed: int): """插入测试结果到 tb_state_tst""" pool = await get_pool() async with pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute( """INSERT INTO tb_state_tst (dnt_id, dpg430_addr, pcnum, serialnum, sub_type, str_type, iffinish, fault_info, relay_out, ppvalue, idle_freq, enter_freq, exit_freq, enter_dist, exit_dist, enter_speed, exit_speed) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""", (dnt_id, dpg430_addr, pcnum, serialnum, sub_type, str_type, iffinish, fault_info, relay_out, ppvalue, idle_freq, enter_freq, exit_freq, enter_dist, exit_dist, enter_speed, exit_speed), ) async def set_device_offline(serial: str): """标记设备离线""" pool = await get_pool() async with pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute( "UPDATE dnt_info SET state=0, last_off=NOW() WHERE serial=%s", (serial,), )