linkrecorddbmgr.h
c
#pragma once
#include <QSqlDatabase>
#include <QSqlError>
#include <QDateTime>
#include <QByteArray>
#include <QString>
#include <QStringList>
#include <QList>
#include <QMap>
#include <QMutex>
#include <QMutexLocker>
// =============================================================================
// 数据结构
// ------ 如果你的项目里已在别处定义,请删除本节,只保留类定义。
// =============================================================================
/// 一条链路质量记录
struct Record
{
QString sendDev; ///< 发送方设备
QString recDev; ///< 接收方设备(同时决定表名)
double score = 0.0;
double packetErrorRate = 0.0;
double bitErrorRate = 0.0;
double snr = 0.0;
double sir = 0.0;
QDateTime datetime;
};
/// 一条原始报文记录
struct RecieveMsg
{
QString sendDev;
QString recDev;
QByteArray msgBytes; ///< 二进制报文
QDateTime datetime;
};
// =============================================================================
// LinkRecordDbMgr --- 数据链通信记录数据库管理器(单例)
//
// * 每个接收设备一张质量表 <recDev>,一张报文表 <recDev>_msg;
// * 所有 SQL 使用 prepare + bindValue,杜绝注入;
// * 所有对外接口线程安全(内部 QMutex);
// * 时间统一以 "yyyy-MM-dd HH:mm:ss" 存储;
// =============================================================================
class LinkRecordDbMgr
{
public:
static LinkRecordDbMgr &getInstance();
// -------- 初始化 --------
QSqlError initDb();
// -------- 查询 --------
/// 返回: recDev -> [sendDev1, sendDev2, ...]
QMap<QString, QStringList> getDevices();
/// 查质量记录。
/// @param recDev 为空时,遍历所有质量表(即"不限接收设备")。
QList<Record> search(const QString &sendDev,
const QString &recDev,
const QDateTime &start,
const QDateTime &end);
/// 查原始报文。
/// @param recDev 必须非空(报文只按接收设备分表)。
QList<RecieveMsg> searchMsg(const QString &sendDev,
const QString &recDev,
const QDateTime &start,
const QDateTime &end);
// -------- 插入 --------
QSqlError insertRecord(const Record &record);
QSqlError insertMsg(const RecieveMsg &record);
// -------- 删除 --------
/// 删除 (sendDev, recDev) 的所有质量记录,表保留。
bool removeRecord(const QString &sendDev, const QString &recDev);
/// 清空该设备的全部质量记录与报文记录(只 DELETE,不 DROP)。
/// @return 是否全部成功
bool removeAllRecord(const QString &recDev);
private:
LinkRecordDbMgr();
~LinkRecordDbMgr() = default;
Q_DISABLE_COPY_MOVE(LinkRecordDbMgr)
// -------- 内部工具 --------
/// 安全引用表名/列名: 在两端加双引号,内部双引号转义为 ""
static QString quoteIdentifier(const QString &ident);
/// 时间格式化常量
static QString fmtDateTime(const QDateTime &dt);
/// 当前数据库里所有"质量表"(非 sqlite_sequence / 非 _msg 后缀)
QStringList recordTables() const;
/// 表是否存在(不区分大小写)
bool tableExists(const QString &name) const;
/// 确保质量表存在,不存在则创建(含索引)
bool ensureRecordTable(const QString &recDev, QSqlError *err = nullptr);
/// 确保报文表存在,不存在则创建(含索引)
bool ensureMsgTable(const QString &recDev, QSqlError *err = nullptr);
private:
QSqlDatabase m_db;
mutable QMutex m_lock;
QString m_connName = "link_rec_record";
QString m_dbPath;
};
linkrecorddbmgr.cpp
cpp
#include "linkrecorddbmgr.h"
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QSqlQuery>
#include <QVariant>
#include <QDebug>
#include <algorithm>
namespace {
/// 时间字符串格式(SQLite 里按 TEXT 存储,按字典序即为时间序)
const char *kDtFmt = "yyyy-MM-dd HH:mm:ss";
} // namespace
// =============================================================================
// 单例 & 构造 / 析构
// =============================================================================
LinkRecordDbMgr &LinkRecordDbMgr::getInstance()
{
static LinkRecordDbMgr inst;
return inst;
}
LinkRecordDbMgr::LinkRecordDbMgr()
{
// 构造即初始化;失败会在日志里打印,外部仍可通过 initDb() 重试。
initDb();
}
// =============================================================================
// initDb
// =============================================================================
QSqlError LinkRecordDbMgr::initDb()
{
// 1. 确保 DB 目录存在
const QString appDir = QCoreApplication::applicationDirPath();
const QString dbDir = appDir + "/DB";
QDir dir;
if (!dir.exists(dbDir)) {
if (!dir.mkpath(dbDir)) {
return QSqlError("LinkRecordDbMgr", "mkpath failed: " + dbDir,
QSqlError::ConnectionError);
}
}
m_dbPath = dbDir + "/link_rec_record.db";
// 2. 复用或创建命名连接
if (QSqlDatabase::contains(m_connName)) {
m_db = QSqlDatabase::database(m_connName, /*open=*/false);
} else {
m_db = QSqlDatabase::addDatabase("QSQLITE", m_connName);
m_db.setDatabaseName(m_dbPath);
}
// 3. 打开(如果尚未打开)
if (!m_db.isOpen() && !m_db.open()) {
const QSqlError e = m_db.lastError();
qWarning() << "[LinkRecordDbMgr] open db failed:" << e.text();
return e;
}
// 4. 打开外键(如有需要)与 WAL 模式(高并发推荐)
QSqlQuery q(m_db);
q.exec("PRAGMA foreign_keys = ON");
q.exec("PRAGMA journal_mode = WAL");
qDebug() << "[LinkRecordDbMgr] db ready at" << m_dbPath;
return {};
}
// =============================================================================
// 查询: getDevices
// =============================================================================
QMap<QString, QStringList> LinkRecordDbMgr::getDevices()
{
QMutexLocker locker(&m_lock);
QMap<QString, QStringList> devMap;
const QStringList tables = recordTables();
for (const QString &recDev : tables) {
QSqlQuery q(m_db);
// 用 DISTINCT 让数据库去重,不再把整表取回来做 QSet
const QString sql = QString("SELECT DISTINCT sendDev FROM %1")
.arg(quoteIdentifier(recDev));
if (!q.exec(sql)) {
qWarning() << "[LinkRecordDbMgr] getDevices failed on"
<< recDev << ":" << q.lastError().text();
continue;
}
QStringList sendDevs;
while (q.next()) {
sendDevs << q.value(0).toString();
}
devMap.insert(recDev, sendDevs);
}
return devMap;
}
// =============================================================================
// 查询: search(质量记录)
// =============================================================================
QList<Record> LinkRecordDbMgr::search(const QString &sendDev,
const QString &recDev,
const QDateTime &start,
const QDateTime &end)
{
QMutexLocker locker(&m_lock);
QList<Record> rst;
// 需要查的表集合:recDev 为空 → 全部质量表
QStringList tables;
if (!recDev.isEmpty()) {
tables << recDev;
} else {
tables = recordTables();
}
const QString sStart = fmtDateTime(start);
const QString sEnd = fmtDateTime(end);
for (const QString &table : tables) {
if (!tableExists(table)) {
continue;
}
QSqlQuery q(m_db);
const QString sql =
QString("SELECT sendDev, recDev, score, packetErrorRate, "
" bitErrorRate, snr, sir, datetime "
"FROM %1 "
"WHERE sendDev = :sendDev "
" AND datetime >= :start "
" AND datetime <= :end "
"ORDER BY datetime ASC")
.arg(quoteIdentifier(table));
q.prepare(sql);
q.bindValue(":sendDev", sendDev);
q.bindValue(":start", sStart);
q.bindValue(":end", sEnd);
if (!q.exec()) {
qWarning() << "[LinkRecordDbMgr] search failed on"
<< table << ":" << q.lastError().text();
continue;
}
while (q.next()) {
Record r;
r.sendDev = q.value(0).toString();
r.recDev = q.value(1).toString();
r.score = q.value(2).toDouble();
r.packetErrorRate = q.value(3).toDouble();
r.bitErrorRate = q.value(4).toDouble();
r.snr = q.value(5).toDouble();
r.sir = q.value(6).toDouble();
r.datetime = q.value(7).toDateTime();
rst << r;
}
}
// 多表合并后重新按时间排序(单表时此处开销极小)
std::sort(rst.begin(), rst.end(),
[](const Record &a, const Record &b) {
return a.datetime < b.datetime;
});
return rst;
}
// =============================================================================
// 查询: searchMsg(报文)
// =============================================================================
QList<RecieveMsg> LinkRecordDbMgr::searchMsg(const QString &sendDev,
const QString &recDev,
const QDateTime &start,
const QDateTime &end)
{
QMutexLocker locker(&m_lock);
QList<RecieveMsg> rst;
if (recDev.isEmpty()) {
qWarning() << "[LinkRecordDbMgr] searchMsg: recDev is empty";
return rst;
}
const QString table = recDev + "_msg";
if (!tableExists(table)) {
return rst;
}
QSqlQuery q(m_db);
const QString sql =
QString("SELECT sendDev, recDev, msgBytes, datetime "
"FROM %1 "
"WHERE sendDev = :sendDev "
" AND datetime >= :start "
" AND datetime <= :end "
"ORDER BY datetime ASC")
.arg(quoteIdentifier(table));
q.prepare(sql);
q.bindValue(":sendDev", sendDev);
q.bindValue(":start", fmtDateTime(start));
q.bindValue(":end", fmtDateTime(end));
if (!q.exec()) {
qWarning() << "[LinkRecordDbMgr] searchMsg failed on"
<< table << ":" << q.lastError().text();
return rst;
}
while (q.next()) {
RecieveMsg m;
m.sendDev = q.value(0).toString();
m.recDev = q.value(1).toString();
m.msgBytes = q.value(2).toByteArray();
m.datetime = q.value(3).toDateTime();
rst << m;
}
return rst;
}
// =============================================================================
// 插入: 质量记录
// =============================================================================
QSqlError LinkRecordDbMgr::insertRecord(const Record &record)
{
QMutexLocker locker(&m_lock);
if (record.recDev.isEmpty()) {
return QSqlError("LinkRecordDbMgr", "recDev is empty",
QSqlError::StatementError);
}
// 1. 确保表存在
QSqlError e;
if (!ensureRecordTable(record.recDev, &e)) {
return e;
}
// 2. 参数化插入
QSqlQuery q(m_db);
const QString sql =
QString("INSERT INTO %1 "
"(recDev, sendDev, score, packetErrorRate, "
" bitErrorRate, snr, sir, datetime) "
"VALUES (:recDev, :sendDev, :score, :packetErrorRate, "
" :bitErrorRate, :snr, :sir, :datetime)")
.arg(quoteIdentifier(record.recDev));
q.prepare(sql);
q.bindValue(":recDev", record.recDev);
q.bindValue(":sendDev", record.sendDev);
q.bindValue(":score", record.score);
q.bindValue(":packetErrorRate", record.packetErrorRate);
q.bindValue(":bitErrorRate", record.bitErrorRate);
q.bindValue(":snr", record.snr);
q.bindValue(":sir", record.sir);
q.bindValue(":datetime", fmtDateTime(record.datetime));
if (!q.exec()) {
const QSqlError err = q.lastError();
qWarning() << "[LinkRecordDbMgr] insertRecord failed:"
<< err.text();
return err;
}
return {};
}
// =============================================================================
// 插入: 报文
// =============================================================================
QSqlError LinkRecordDbMgr::insertMsg(const RecieveMsg &record)
{
QMutexLocker locker(&m_lock);
if (record.recDev.isEmpty()) {
return QSqlError("LinkRecordDbMgr", "recDev is empty",
QSqlError::StatementError);
}
QSqlError e;
if (!ensureMsgTable(record.recDev, &e)) {
return e;
}
QSqlQuery q(m_db);
const QString table = record.recDev + "_msg";
const QString sql =
QString("INSERT INTO %1 (recDev, sendDev, msgBytes, datetime) "
"VALUES (:recDev, :sendDev, :msgBytes, :datetime)")
.arg(quoteIdentifier(table));
q.prepare(sql);
q.bindValue(":recDev", record.recDev);
q.bindValue(":sendDev", record.sendDev);
q.bindValue(":msgBytes", record.msgBytes);
q.bindValue(":datetime", fmtDateTime(record.datetime));
if (!q.exec()) {
const QSqlError err = q.lastError();
qWarning() << "[LinkRecordDbMgr] insertMsg failed:" << err.text();
return err;
}
return {};
}
// =============================================================================
// 删除
// =============================================================================
bool LinkRecordDbMgr::removeRecord(const QString &sendDev,
const QString &recDev)
{
QMutexLocker locker(&m_lock);
if (recDev.isEmpty()) {
return false;
}
if (!tableExists(recDev)) {
return true; // 表本来就不存在,视为"已删除"
}
QSqlQuery q(m_db);
const QString sql = QString("DELETE FROM %1 "
"WHERE sendDev = :sendDev")
.arg(quoteIdentifier(recDev));
q.prepare(sql);
q.bindValue(":sendDev", sendDev);
if (!q.exec()) {
qWarning() << "[LinkRecordDbMgr] removeRecord failed:"
<< q.lastError().text();
return false;
}
return true;
}
bool LinkRecordDbMgr::removeAllRecord(const QString &recDev)
{
QMutexLocker locker(&m_lock);
if (recDev.isEmpty()) {
return false;
}
bool ok = true;
// 质量表:DELETE
if (tableExists(recDev)) {
QSqlQuery q(m_db);
if (!q.exec(QString("DELETE FROM %1").arg(quoteIdentifier(recDev)))) {
qWarning() << "[LinkRecordDbMgr] removeAllRecord(quality) failed:"
<< q.lastError().text();
ok = false;
}
}
// 报文表:DELETE
const QString msgTable = recDev + "_msg";
if (tableExists(msgTable)) {
QSqlQuery q(m_db);
if (!q.exec(QString("DELETE FROM %1").arg(quoteIdentifier(msgTable)))) {
qWarning() << "[LinkRecordDbMgr] removeAllRecord(msg) failed:"
<< q.lastError().text();
ok = false;
}
}
// 回收磁盘空间(可选,数据量大时才有意义)
if (ok) {
QSqlQuery(m_db).exec("VACUUM");
}
return ok;
}
// =============================================================================
// 内部工具
// =============================================================================
QString LinkRecordDbMgr::quoteIdentifier(const QString &ident)
{
QString s = ident;
s.replace('"', "\"\""); // 内部双引号转义为 ""
return "\"" + s + "\"";
}
QString LinkRecordDbMgr::fmtDateTime(const QDateTime &dt)
{
return dt.toString(kDtFmt);
}
QStringList LinkRecordDbMgr::recordTables() const
{
QStringList rst;
const QStringList all = m_db.tables();
for (const QString &t : all) {
if (t.compare("sqlite_sequence", Qt::CaseInsensitive) == 0) continue;
if (t.endsWith("_msg", Qt::CaseInsensitive)) continue;
rst << t;
}
return rst;
}
bool LinkRecordDbMgr::tableExists(const QString &name) const
{
return m_db.tables().contains(name, Qt::CaseInsensitive);
}
bool LinkRecordDbMgr::ensureRecordTable(const QString &recDev, QSqlError *err)
{
if (tableExists(recDev)) {
return true;
}
const QString sql =
QString("CREATE TABLE IF NOT EXISTS %1 ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" sendDev TEXT NOT NULL,"
" recDev TEXT NOT NULL,"
" score DOUBLE,"
" packetErrorRate DOUBLE,"
" bitErrorRate DOUBLE,"
" snr DOUBLE,"
" sir DOUBLE,"
" datetime DATETIME"
")").arg(quoteIdentifier(recDev));
QSqlQuery q(m_db);
if (!q.exec(sql)) {
if (err) *err = q.lastError();
qWarning() << "[LinkRecordDbMgr] ensureRecordTable failed:"
<< q.lastError().text();
return false;
}
// 常用查询索引
q.exec(QString("CREATE INDEX IF NOT EXISTS %1 "
"ON %2 (sendDev, datetime)")
.arg(quoteIdentifier(recDev + "_idx_send_dt"),
quoteIdentifier(recDev)));
return true;
}
bool LinkRecordDbMgr::ensureMsgTable(const QString &recDev, QSqlError *err)
{
const QString table = recDev + "_msg";
if (tableExists(table)) {
return true;
}
const QString sql =
QString("CREATE TABLE IF NOT EXISTS %1 ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" sendDev TEXT NOT NULL,"
" recDev TEXT NOT NULL,"
" msgBytes BLOB,"
" datetime DATETIME"
")").arg(quoteIdentifier(table));
QSqlQuery q(m_db);
if (!q.exec(sql)) {
if (err) *err = q.lastError();
qWarning() << "[LinkRecordDbMgr] ensureMsgTable failed:"
<< q.lastError().text();
return false;
}
q.exec(QString("CREATE INDEX IF NOT EXISTS %1 "
"ON %2 (sendDev, datetime)")
.arg(quoteIdentifier(table + "_idx_send_dt"),
quoteIdentifier(table)));
return true;
}
使用示范
cpp
auto &dbMgr = LinkRecordDbMgr::getInstance();
// 插入一条质量记录
Record r;
r.sendDev = "radio-001";
r.recDev = "radio-002";
r.score = 95.5; r.snr = 12.4; r.sir = 20.1;
r.packetErrorRate = 0.001; r.bitErrorRate = 1e-5;
r.datetime = QDateTime::currentDateTime();
dbMgr.insertRecord(r);
// 插入一条报文
RecieveMsg m;
m.sendDev = "radio-001";
m.recDev = "radio-002";
m.msgBytes = QByteArray::fromHex("01020304");
m.datetime = QDateTime::currentDateTime();
dbMgr.insertMsg(m);
// 查某对设备在某个时间段的记录
const auto list = dbMgr.search("radio-001", "radio-002",
QDateTime::currentDateTime().addDays(-1),
QDateTime::currentDateTime());
for (const auto &rec : list) {
qDebug() << rec.datetime << rec.snr << rec.sir;
}
// 列出现有设备
const auto devMap = dbMgr.getDevices();
for (auto it = devMap.begin(); it != devMap.end(); ++it) {
qDebug() << "recDev:" << it.key() << " senders:" << it.value();
}
// 清空 radio-002 的全部记录(表保留)
dbMgr.removeAllRecord("radio-002");