feat: EDC 服务 — Python/uvloop 实现,UDP/TCP 异步网络服务
This commit is contained in:
268
src/models.py
Normal file
268
src/models.py
Normal file
@@ -0,0 +1,268 @@
|
||||
"""数据库模型 — 连接池管理与表结构初始化"""
|
||||
|
||||
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,),
|
||||
)
|
||||
Reference in New Issue
Block a user