From c62dbec7328a8b44e6ec61758e7b8463f2e502dd Mon Sep 17 00:00:00 2001
From: LAPTOP-SNT8I5JK\Boounion <Chenluhua@qq.com>
Date: 星期五, 12 九月 2025 11:58:15 +0800
Subject: [PATCH] Merge branch 'liuyang'

---
 SourceCode/Bond/Servo/GlassLogDb.cpp |  526 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 526 insertions(+), 0 deletions(-)

diff --git a/SourceCode/Bond/Servo/GlassLogDb.cpp b/SourceCode/Bond/Servo/GlassLogDb.cpp
new file mode 100644
index 0000000..b9f0df5
--- /dev/null
+++ b/SourceCode/Bond/Servo/GlassLogDb.cpp
@@ -0,0 +1,526 @@
+// GlassLogDb.cpp - 单例封装:SQLite 写入/查询/分页/统计/CSV 导出(已对接 SERVO::CGlass)
+#include "stdafx.h"
+#include "GlassLogDb.h"
+#include "sqlite3.h"
+#include <stdexcept>
+#include <sstream>
+#include <iomanip>
+#include <ctime>
+#include <fstream>
+#include "GlassJson.h"
+
+
+using namespace SERVO;
+
+#ifndef GLASS_LOG_TABLE
+#define GLASS_LOG_TABLE "glass_log"
+#endif
+
+// ================== 工具函数 ==================
+static void throwIf(int rc, sqlite3* db, const char* msg) {
+    if (rc != SQLITE_OK && rc != SQLITE_DONE && rc != SQLITE_ROW) {
+        std::ostringstream oss;
+        oss << msg << " (rc=" << rc << "): " << (db ? sqlite3_errmsg(db) : "null db");
+        throw std::runtime_error(oss.str());
+    }
+}
+static inline const char* safe_text(sqlite3_stmt* s, int col) {
+    const unsigned char* p = sqlite3_column_text(s, col);
+    return p ? reinterpret_cast<const char*>(p) : "";
+}
+static std::string csvEscape(const std::string& s) {
+    bool needQuote = s.find_first_of(",\"\n\r") != std::string::npos;
+    if (!needQuote) return s;
+    std::string out; out.reserve(s.size() + 2);
+    out.push_back('"');
+    for (char c : s) out += (c == '"') ? "\"\"" : std::string(1, c);
+    out.push_back('"');
+    return out;
+}
+static std::string toIso8601String(const std::optional<std::chrono::system_clock::time_point>& tp) {
+    if (!tp.has_value()) return "";
+    using namespace std::chrono;
+    auto t = system_clock::to_time_t(*tp);
+    std::tm tm{};
+#if defined(_WIN32)
+    gmtime_s(&tm, &t);
+#else
+    gmtime_r(&t, &tm);
+#endif
+    std::ostringstream oss;
+    oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%SZ");
+    return oss.str();
+}
+
+// ================== 单例静态成员 ==================
+std::unique_ptr<GlassLogDb> GlassLogDb::s_inst;
+std::mutex GlassLogDb::s_instMtx;
+
+// ================== 单例接口实现 ==================
+void GlassLogDb::Init(const std::string& dbPath) {
+    std::lock_guard<std::mutex> g(s_instMtx);
+    if (!s_inst) {
+        s_inst.reset(new GlassLogDb(dbPath));
+    }
+    else if (s_inst->dbPath_ != dbPath) {
+        s_inst->reopenInternal(dbPath);
+    }
+}
+GlassLogDb& GlassLogDb::Instance() {
+    std::lock_guard<std::mutex> g(s_instMtx);
+    if (!s_inst) throw std::runtime_error("GlassLogDb::Instance() called before Init()");
+    return *s_inst;
+}
+bool GlassLogDb::IsInitialized() noexcept {
+    std::lock_guard<std::mutex> g(s_instMtx);
+    return static_cast<bool>(s_inst);
+}
+void GlassLogDb::Reopen(const std::string& dbPath) {
+    std::lock_guard<std::mutex> g(s_instMtx);
+    if (!s_inst) {
+        s_inst.reset(new GlassLogDb(dbPath));
+    }
+    else {
+        s_inst->reopenInternal(dbPath);
+    }
+}
+std::string GlassLogDb::CurrentPath() {
+    std::lock_guard<std::mutex> g(s_instMtx);
+    return s_inst ? s_inst->dbPath_ : std::string();
+}
+
+// ================== 构造/打开/关闭/重开 ==================
+GlassLogDb::GlassLogDb(const std::string& dbPath) {
+    openDb(dbPath);
+}
+void GlassLogDb::openDb(const std::string& dbPath) {
+    int rc = sqlite3_open_v2(dbPath.c_str(), &db_,
+        SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
+        nullptr);
+    if (rc != SQLITE_OK) {
+        std::string err = db_ ? sqlite3_errmsg(db_) : "open failed";
+        if (db_) sqlite3_close(db_);
+        db_ = nullptr;
+        throw std::runtime_error("Failed to open sqlite DB: " + err + " | path=" + dbPath);
+    }
+    dbPath_ = dbPath;
+    ensureSchema();
+    prepareStatements();
+}
+void GlassLogDb::closeDb() noexcept {
+    finalizeStatements();
+    if (db_) {
+        sqlite3_close(db_);
+        db_ = nullptr;
+    }
+}
+void GlassLogDb::reopenInternal(const std::string& dbPath) {
+    std::lock_guard<std::mutex> lk(mtx_);
+    if (dbPath == dbPath_) return;
+    closeDb();
+    openDb(dbPath);
+}
+
+// ================== 析构 ==================
+GlassLogDb::~GlassLogDb() {
+    closeDb();
+}
+
+// ================== DDL & 预编译 ==================
+void GlassLogDb::ensureSchema() {
+    const char* ddl =
+        "CREATE TABLE IF NOT EXISTS " GLASS_LOG_TABLE " ("
+        "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
+        "  cassette_seq_no INTEGER,"
+        "  job_seq_no INTEGER,"
+        "  class_id TEXT,"
+        "  material_type INTEGER,"
+        "  state INTEGER,"
+        "  t_start TEXT,"      // ISO8601 UTC(可为 NULL)
+        "  t_end TEXT,"        // ISO8601 UTC(可为 NULL)
+        "  buddy_id TEXT,"
+        "  aoi_result INTEGER,"
+        "  path TEXT,"
+        "  params TEXT,"
+        "  pretty TEXT"
+        ");"
+        "CREATE INDEX IF NOT EXISTS idx_glass_key "
+        "  ON " GLASS_LOG_TABLE " (cassette_seq_no, job_seq_no);"
+        "CREATE INDEX IF NOT EXISTS idx_class_id "
+        "  ON " GLASS_LOG_TABLE " (class_id);"
+        "CREATE INDEX IF NOT EXISTS idx_t_start "
+        "  ON " GLASS_LOG_TABLE " (t_start);";
+
+    char* errMsg = nullptr;
+    int rc = sqlite3_exec(db_, ddl, nullptr, nullptr, &errMsg);
+    if (rc != SQLITE_OK) {
+        std::string err = errMsg ? errMsg : "unknown";
+        sqlite3_free(errMsg);
+        throw std::runtime_error("Failed to create schema: " + err);
+    }
+}
+void GlassLogDb::prepareStatements() {
+    const char* sql =
+        "INSERT INTO " GLASS_LOG_TABLE " ("
+        " cassette_seq_no, job_seq_no, class_id, material_type, state,"
+        " t_start, t_end, buddy_id, aoi_result, path, params, pretty)"
+        " VALUES (?,?,?,?,?,?,?,?,?,?,?,?);";
+    int rc = sqlite3_prepare_v2(db_, sql, -1, &stmtInsert_, nullptr);
+    throwIf(rc, db_, "prepare insert");
+}
+void GlassLogDb::finalizeStatements() noexcept {
+    if (stmtInsert_) {
+        sqlite3_finalize(stmtInsert_);
+        stmtInsert_ = nullptr;
+    }
+}
+
+// ================== 事务 ==================
+void GlassLogDb::beginTransaction() {
+    std::lock_guard<std::mutex> lk(mtx_);
+    char* err = nullptr;
+    int rc = sqlite3_exec(db_, "BEGIN IMMEDIATE;", nullptr, nullptr, &err);
+    if (rc != SQLITE_OK) {
+        std::string e = err ? err : "unknown";
+        sqlite3_free(err);
+        throw std::runtime_error("BEGIN failed: " + e);
+    }
+}
+void GlassLogDb::commit() {
+    std::lock_guard<std::mutex> lk(mtx_);
+    char* err = nullptr;
+    int rc = sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, &err);
+    if (rc != SQLITE_OK) {
+        std::string e = err ? err : "unknown";
+        sqlite3_free(err);
+        throw std::runtime_error("COMMIT failed: " + e);
+    }
+}
+void GlassLogDb::rollback() {
+    std::lock_guard<std::mutex> lk(mtx_);
+    char* err = nullptr;
+    sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, &err);
+    if (err) sqlite3_free(err);
+}
+
+// ================== 时间格式 ==================
+std::string GlassLogDb::toIso8601Utc(std::chrono::system_clock::time_point tp) {
+    using namespace std::chrono;
+    auto t = system_clock::to_time_t(tp);
+    std::tm tm{};
+#if defined(_WIN32)
+    gmtime_s(&tm, &t);
+#else
+    gmtime_r(&t, &tm);
+#endif
+    std::ostringstream oss;
+    oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%SZ");
+    return oss.str();
+}
+
+// ================== 插入实现 ==================
+long long GlassLogDb::doInsert(
+    int cassetteSeqNo,
+    int jobSeqNo,
+    const std::string& classId,
+    int materialType,
+    int state,
+    std::optional<std::chrono::system_clock::time_point> tStart,
+    std::optional<std::chrono::system_clock::time_point> tEnd,
+    const std::string& buddyId,
+    int aoiResult,
+    const std::string& pathDesc,
+    const std::string& paramsDesc,
+    const std::string& prettyString)
+{
+    std::lock_guard<std::mutex> lk(mtx_);
+    int idx = 1;
+    throwIf(sqlite3_reset(stmtInsert_), db_, "reset insert");
+    throwIf(sqlite3_clear_bindings(stmtInsert_), db_, "clear bindings");
+
+    throwIf(sqlite3_bind_int(stmtInsert_, idx++, cassetteSeqNo), db_, "bind cassette");
+    throwIf(sqlite3_bind_int(stmtInsert_, idx++, jobSeqNo), db_, "bind job");
+    throwIf(sqlite3_bind_text(stmtInsert_, idx++, classId.c_str(), -1, SQLITE_TRANSIENT), db_, "bind class_id");
+    throwIf(sqlite3_bind_int(stmtInsert_, idx++, materialType), db_, "bind material_type");
+    throwIf(sqlite3_bind_int(stmtInsert_, idx++, state), db_, "bind state");
+
+    if (tStart.has_value()) {
+        auto s = toIso8601Utc(*tStart);
+        throwIf(sqlite3_bind_text(stmtInsert_, idx++, s.c_str(), -1, SQLITE_TRANSIENT), db_, "bind t_start");
+    }
+    else {
+        throwIf(sqlite3_bind_null(stmtInsert_, idx++), db_, "bind t_start null");
+    }
+    if (tEnd.has_value()) {
+        auto e = toIso8601Utc(*tEnd);
+        throwIf(sqlite3_bind_text(stmtInsert_, idx++, e.c_str(), -1, SQLITE_TRANSIENT), db_, "bind t_end");
+    }
+    else {
+        throwIf(sqlite3_bind_null(stmtInsert_, idx++), db_, "bind t_end null");
+    }
+
+    throwIf(sqlite3_bind_text(stmtInsert_, idx++, buddyId.c_str(), -1, SQLITE_TRANSIENT), db_, "bind buddy_id");
+    throwIf(sqlite3_bind_int(stmtInsert_, idx++, aoiResult), db_, "bind aoi_result");
+    throwIf(sqlite3_bind_text(stmtInsert_, idx++, pathDesc.c_str(), -1, SQLITE_TRANSIENT), db_, "bind path");
+    throwIf(sqlite3_bind_text(stmtInsert_, idx++, paramsDesc.c_str(), -1, SQLITE_TRANSIENT), db_, "bind params");
+    throwIf(sqlite3_bind_text(stmtInsert_, idx++, prettyString.c_str(), -1, SQLITE_TRANSIENT), db_, "bind pretty");
+
+    int rc = sqlite3_step(stmtInsert_);
+    throwIf(rc, db_, "insert step");
+    return sqlite3_last_insert_rowid(db_);
+}
+
+long long GlassLogDb::insertFromCGlass(CGlass& g)
+{
+    const int cassette = static_cast<int>(g.getCassetteSequenceNo());
+    const int job = static_cast<int>(g.getJobSequenceNo());
+    const std::string& classId = g.getID();
+    const int material = static_cast<int>(g.getType());
+    const int state = static_cast<int>(g.state());
+    const std::string& buddy = g.getBuddyId();
+    const int aoiResult = static_cast<int>(g.getAOIInspResult());
+    const std::string pathDesc = g.getPathDescription();
+    const std::string paramsDesc = g.getParamsDescription();
+    const std::string pretty = GlassJson::ToPrettyString(g);
+
+    auto tStart = g.tStart();
+    auto tEnd = g.tEnd();
+
+    return doInsert(cassette, job, classId, material, state,
+        tStart, tEnd, buddy, aoiResult, pathDesc, paramsDesc, pretty);
+}
+
+long long GlassLogDb::insertExplicit(
+    int cassetteSeqNo,
+    int jobSeqNo,
+    const std::string& classId,
+    int materialType,
+    int state,
+    std::optional<std::chrono::system_clock::time_point> tStart,
+    std::optional<std::chrono::system_clock::time_point> tEnd,
+    const std::string& buddyId,
+    int aoiResult,
+    const std::string& pathDesc,
+    const std::string& paramsDesc,
+    const std::string& prettyString)
+{
+    return doInsert(cassetteSeqNo, jobSeqNo, classId, materialType, state,
+        tStart, tEnd, buddyId, aoiResult, pathDesc, paramsDesc, prettyString);
+}
+
+// ================== 过滤构造(where + bind) ==================
+static std::string buildWhereSql(const GlassLogDb::Filters& f) {
+    std::ostringstream where;
+    bool first = true;
+    auto add = [&](const std::string& cond) {
+        if (first) { where << " WHERE " << cond; first = false; }
+        else { where << " AND " << cond; }
+    };
+    if (f.classId)       add("class_id = ?");
+    if (f.cassetteSeqNo) add("cassette_seq_no = ?");
+    if (f.jobSeqNo)      add("job_seq_no = ?");
+    if (f.keyword)       add("(class_id LIKE ? OR buddy_id LIKE ? OR path LIKE ? OR params LIKE ? OR pretty LIKE ?)");
+    if (f.tStartFrom && f.tStartTo) add("(t_start >= ? AND t_start <= ?)");
+    else if (f.tStartFrom)          add("t_start >= ?");
+    else if (f.tStartTo)            add("t_start <= ?");
+    return where.str();
+}
+static void bindFilters(sqlite3_stmt* stmt, sqlite3* db, int& idx, const GlassLogDb::Filters& f) {
+    if (f.classId)       throwIf(sqlite3_bind_text(stmt, idx++, f.classId->c_str(), -1, SQLITE_TRANSIENT), db, "bind classId");
+    if (f.cassetteSeqNo) throwIf(sqlite3_bind_int(stmt, idx++, *f.cassetteSeqNo), db, "bind cassette");
+    if (f.jobSeqNo)      throwIf(sqlite3_bind_int(stmt, idx++, *f.jobSeqNo), db, "bind job");
+    if (f.keyword) {
+        std::string kw = "%" + *f.keyword + "%";
+        for (int i = 0; i < 5; ++i)
+            throwIf(sqlite3_bind_text(stmt, idx++, kw.c_str(), -1, SQLITE_TRANSIENT), db, "bind keyword");
+    }
+    if (f.tStartFrom && f.tStartTo) {
+        std::string s = toIso8601String(f.tStartFrom);
+        std::string e = toIso8601String(f.tStartTo);
+        throwIf(sqlite3_bind_text(stmt, idx++, s.c_str(), -1, SQLITE_TRANSIENT), db, "bind tStartFrom");
+        throwIf(sqlite3_bind_text(stmt, idx++, e.c_str(), -1, SQLITE_TRANSIENT), db, "bind tStartTo");
+    }
+    else if (f.tStartFrom) {
+        std::string s = toIso8601String(f.tStartFrom);
+        throwIf(sqlite3_bind_text(stmt, idx++, s.c_str(), -1, SQLITE_TRANSIENT), db, "bind tStartFrom");
+    }
+    else if (f.tStartTo) {
+        std::string e = toIso8601String(f.tStartTo);
+        throwIf(sqlite3_bind_text(stmt, idx++, e.c_str(), -1, SQLITE_TRANSIENT), db, "bind tStartTo");
+    }
+}
+
+// ================== 查询 / 统计 / 分页 ==================
+std::vector<GlassLogDb::Row> GlassLogDb::query(
+    const Filters& filters,
+    int limit,
+    int offset)
+{
+    std::lock_guard<std::mutex> lk(mtx_);
+
+    std::ostringstream sql;
+    sql << "SELECT id, cassette_seq_no, job_seq_no, class_id, material_type, state,"
+        " IFNULL(strftime('%Y-%m-%d %H:%M:%S', t_start, 'localtime'), ''),"
+        " IFNULL(strftime('%Y-%m-%d %H:%M:%S', t_end,   'localtime'), ''),"
+        " buddy_id, aoi_result, path, params, pretty"
+        " FROM " GLASS_LOG_TABLE;
+
+    std::string where = buildWhereSql(filters);
+    sql << where
+        << " ORDER BY id DESC"
+        << " LIMIT " << (limit < 0 ? 0 : limit)
+        << " OFFSET " << (offset < 0 ? 0 : offset);
+
+    sqlite3_stmt* stmt = nullptr;
+    throwIf(sqlite3_prepare_v2(db_, sql.str().c_str(), -1, &stmt, nullptr), db_, "prepare query");
+
+    int idx = 1;
+    bindFilters(stmt, db_, idx, filters);
+
+    std::vector<Row> rows;
+    for (;;) {
+        int rc = sqlite3_step(stmt);
+        if (rc == SQLITE_ROW) {
+            Row r;
+            r.id = sqlite3_column_int64(stmt, 0);
+            r.cassetteSeqNo = sqlite3_column_int(stmt, 1);
+            r.jobSeqNo = sqlite3_column_int(stmt, 2);
+            r.classId = safe_text(stmt, 3);
+            r.materialType = sqlite3_column_int(stmt, 4);
+            r.state = sqlite3_column_int(stmt, 5);
+            r.tStart = safe_text(stmt, 6);
+            r.tEnd = safe_text(stmt, 7);
+            r.buddyId = safe_text(stmt, 8);
+            r.aoiResult = sqlite3_column_int(stmt, 9);
+            r.path = safe_text(stmt, 10);
+            r.params = safe_text(stmt, 11);
+            r.pretty = safe_text(stmt, 12);
+            rows.push_back(std::move(r));
+        }
+        else if (rc == SQLITE_DONE) {
+            break;
+        }
+        else {
+            sqlite3_finalize(stmt);
+            throwIf(rc, db_, "query step");
+        }
+    }
+    sqlite3_finalize(stmt);
+    return rows;
+}
+
+long long GlassLogDb::count(const Filters& filters) {
+    std::lock_guard<std::mutex> lk(mtx_);
+
+    std::ostringstream sql;
+    sql << "SELECT COUNT(1) FROM " GLASS_LOG_TABLE;
+
+    std::string where = buildWhereSql(filters);
+    sql << where;
+
+    sqlite3_stmt* stmt = nullptr;
+    throwIf(sqlite3_prepare_v2(db_, sql.str().c_str(), -1, &stmt, nullptr), db_, "prepare count");
+
+    int idx = 1;
+    bindFilters(stmt, db_, idx, filters);
+
+    long long total = 0;
+    int rc = sqlite3_step(stmt);
+    if (rc == SQLITE_ROW) {
+        total = sqlite3_column_int64(stmt, 0);
+    }
+    else if (rc != SQLITE_DONE) {
+        sqlite3_finalize(stmt);
+        throwIf(rc, db_, "count step");
+    }
+    sqlite3_finalize(stmt);
+    return total;
+}
+
+GlassLogDb::Page GlassLogDb::queryPaged(
+    const Filters& filters,
+    int limit,
+    int offset)
+{
+    Page p;
+    p.limit = (limit < 0 ? 0 : limit);
+    p.offset = (offset < 0 ? 0 : offset);
+    p.total = count(filters);
+    p.items = query(filters, p.limit, p.offset);
+    return p;
+}
+
+// ================== 导出 CSV(全部符合条件) ==================
+long long GlassLogDb::exportCsv(const std::string& csvPath, const Filters& filters) {
+    std::lock_guard<std::mutex> lk(mtx_);
+
+    std::ofstream ofs(csvPath, std::ios::binary);
+    if (!ofs) {
+        throw std::runtime_error("Failed to open csv for write: " + csvPath);
+    }
+
+    // 表头
+    ofs << "id,cassette_seq_no,job_seq_no,class_id,material_type,state,t_start,t_end,"
+        "buddy_id,aoi_result,path,params,pretty\n";
+
+    std::ostringstream sql;
+    sql << "SELECT id, cassette_seq_no, job_seq_no, class_id, material_type, state,"
+        " IFNULL(strftime('%Y-%m-%d %H:%M:%S', t_start, 'localtime'), ''),"
+        " IFNULL(strftime('%Y-%m-%d %H:%M:%S', t_end,   'localtime'), ''),"
+        " buddy_id, aoi_result, path, params, pretty"
+        " FROM " GLASS_LOG_TABLE;
+
+    std::string where = buildWhereSql(filters);
+    sql << where << " ORDER BY id DESC";
+
+    sqlite3_stmt* stmt = nullptr;
+    throwIf(sqlite3_prepare_v2(db_, sql.str().c_str(), -1, &stmt, nullptr), db_, "prepare export");
+
+    int idx = 1;
+    bindFilters(stmt, db_, idx, filters);
+
+    long long rows = 0;
+    for (;;) {
+        int rc = sqlite3_step(stmt);
+        if (rc == SQLITE_ROW) {
+            std::string id = std::to_string(sqlite3_column_int64(stmt, 0));
+            std::string cassette = std::to_string(sqlite3_column_int(stmt, 1));
+            std::string job = std::to_string(sqlite3_column_int(stmt, 2));
+            std::string class_id = safe_text(stmt, 3);
+            std::string material = std::to_string(sqlite3_column_int(stmt, 4));
+            std::string state = std::to_string(sqlite3_column_int(stmt, 5));
+            std::string t_start = safe_text(stmt, 6);
+            std::string t_end = safe_text(stmt, 7);
+            std::string buddy = safe_text(stmt, 8);
+            std::string aoi = std::to_string(sqlite3_column_int(stmt, 9));
+            std::string path = safe_text(stmt, 10);
+            std::string params = safe_text(stmt, 11);
+            std::string pretty = safe_text(stmt, 12);
+
+            ofs << id << ','
+                << cassette << ','
+                << job << ','
+                << csvEscape(class_id) << ','
+                << material << ','
+                << state << ','
+                << t_start << ','
+                << t_end << ','
+                << csvEscape(buddy) << ','
+                << aoi << ','
+                << csvEscape(path) << ','
+                << csvEscape(params) << ','
+                << csvEscape(pretty) << '\n';
+            ++rows;
+        }
+        else if (rc == SQLITE_DONE) {
+            break;
+        }
+        else {
+            sqlite3_finalize(stmt);
+            throwIf(rc, db_, "export step");
+        }
+    }
+    sqlite3_finalize(stmt);
+    ofs.flush();
+    return rows;
+}

--
Gitblit v1.9.3