database.cpp 71 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832
  1. /**
  2. * database.cpp — 数据库实现
  3. */
  4. #include "database.h"
  5. #include "utils.h"
  6. #include <iostream>
  7. #include <thread>
  8. #include <chrono>
  9. // 前向声明(static函数定义在后面,但init_database()需要调用)
  10. static void _db_load_tw_cache_unlocked();
  11. bool check_db_health() {
  12. std::lock_guard<std::mutex> lock(g_db_mtx);
  13. if (!g_db) return false;
  14. sqlite3_stmt* stmt = nullptr;
  15. const char* sql = "SELECT 1;";
  16. int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
  17. if (rc != SQLITE_OK) {
  18. std::cerr << "[数据库健康检查] 失败: " << sqlite3_errmsg(g_db) << std::endl;
  19. return false;
  20. }
  21. rc = sqlite3_step(stmt);
  22. sqlite3_finalize(stmt);
  23. if (rc != SQLITE_ROW) {
  24. std::cerr << "[数据库健康检查] 执行失败" << std::endl;
  25. return false;
  26. }
  27. g_metrics.record_db_health_check();
  28. return true;
  29. }
  30. int init_database() {
  31. std::lock_guard<std::mutex> lock(g_db_mtx);
  32. g_db_path = pathComm + "data/upload_records.db";
  33. int rc = sqlite3_open(g_db_path.c_str(), &g_db);
  34. if (rc) {
  35. std::cerr << "无法打开数据库: " << sqlite3_errmsg(g_db) << std::endl;
  36. g_db = nullptr;
  37. return -1;
  38. }
  39. if (sqlite3_db_readonly(g_db, "main") == 1) {
  40. std::cerr << "数据库为只读,尝试修复权限..." << std::endl;
  41. sqlite3_close(g_db);
  42. chmod(g_db_path.c_str(), 0666);
  43. rc = sqlite3_open_v2(g_db_path.c_str(), &g_db,
  44. SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL);
  45. if (rc != SQLITE_OK) {
  46. std::cerr << "无法以读写方式打开数据库" << std::endl;
  47. g_db = nullptr;
  48. return -1;
  49. }
  50. }
  51. // ✅ v41 优化:SQLite busy_timeout 设为 100ms
  52. sqlite3_busy_timeout(g_db, DB_BUSY_TIMEOUT_MS);
  53. const char* create_table_sql =
  54. "CREATE TABLE IF NOT EXISTS upload_records ("
  55. " id INTEGER PRIMARY KEY AUTOINCREMENT,"
  56. " create_time DATETIME DEFAULT (datetime('now', 'localtime')),"
  57. " capture_time INTEGER," // ✅ fix24-v13: 抓拍时刻Unix时间戳,解决DB插入延迟导致create_time不准
  58. " plate_number TEXT NOT NULL,"
  59. " tb_num TEXT NOT NULL,"
  60. " station_type INTEGER,"
  61. " lo_photo_path TEXT,"
  62. " hi_photo_path TEXT,"
  63. " lo_upload_status INTEGER DEFAULT 0,"
  64. " hi_upload_status INTEGER DEFAULT 0,"
  65. " feishu_status INTEGER DEFAULT 0,"
  66. " retry_count INTEGER DEFAULT 0"
  67. ");";
  68. char* errMsg = nullptr;
  69. rc = sqlite3_exec(g_db, create_table_sql, nullptr, nullptr, &errMsg);
  70. if (rc != SQLITE_OK) {
  71. std::cerr << "创建表失败: " << errMsg << std::endl;
  72. sqlite3_free(errMsg);
  73. return -1;
  74. }
  75. // ✅ fix24-v13: 为已有数据库添加capture_time列(新库已包含,旧库需ALTER)
  76. const char* alter_sql = "ALTER TABLE upload_records ADD COLUMN capture_time INTEGER;";
  77. sqlite3_exec(g_db, alter_sql, nullptr, nullptr, nullptr); // 忽略"duplicate column"错误
  78. // ✅ 新增覆盖所有查询条件的联合索引,解决N+1查询性能问题
  79. const char* create_index_sql =
  80. "CREATE INDEX IF NOT EXISTS idx_plate_number ON upload_records(plate_number);"
  81. "CREATE INDEX IF NOT EXISTS idx_tb_num ON upload_records(tb_num);"
  82. "CREATE INDEX IF NOT EXISTS idx_create_time ON upload_records(create_time);"
  83. "CREATE INDEX IF NOT EXISTS idx_plate_station_time_status "
  84. "ON upload_records(plate_number, station_type, create_time, lo_upload_status, hi_upload_status);";
  85. // ✅ TIME_WINDOW持久化表:存储进站/出站工单配额缓存
  86. const char* create_tw_cache_sql =
  87. "CREATE TABLE IF NOT EXISTS time_window_cache ("
  88. " id INTEGER PRIMARY KEY AUTOINCREMENT,"
  89. " plate_number TEXT NOT NULL,"
  90. " station_type INTEGER NOT NULL," // 0=进站, 1=出站
  91. " create_time INTEGER NOT NULL," // Unix时间戳:首次处理时间
  92. " last_time INTEGER NOT NULL," // Unix时间戳:最后处理时间
  93. " bill_count INTEGER DEFAULT 0," // 工单数量
  94. " photo_count INTEGER DEFAULT 0," // 照片计数(进站用)
  95. " tb_num TEXT," // 最后一次工单编号
  96. " generation INTEGER DEFAULT 1," // 周期代数
  97. " UNIQUE(plate_number, station_type)"
  98. ");";
  99. rc = sqlite3_exec(g_db, create_tw_cache_sql, nullptr, nullptr, &errMsg);
  100. if (rc != SQLITE_OK) {
  101. std::cerr << "创建time_window_cache表失败: " << errMsg << std::endl;
  102. sqlite3_free(errMsg);
  103. } else {
  104. // 创建索引
  105. const char* create_tw_index_sql =
  106. "CREATE INDEX IF NOT EXISTS idx_tw_plate ON time_window_cache(plate_number, station_type);"
  107. "CREATE INDEX IF NOT EXISTS idx_tw_create_time ON time_window_cache(create_time);";
  108. sqlite3_exec(g_db, create_tw_index_sql, nullptr, nullptr, nullptr);
  109. std::cout << "✅ time_window_cache表初始化成功" << std::endl;
  110. } // ✅ v43修复:闭合else块,station_lock_cache不嵌套在内
  111. // ✅ v42新增/v43修复:交替锁定缓存表(独立于time_window_cache的else块)
  112. const char* create_station_lock_sql =
  113. "CREATE TABLE IF NOT EXISTS station_lock_cache ("
  114. " plate_number TEXT PRIMARY KEY,"
  115. " last_in_time INTEGER NOT NULL DEFAULT 0,"
  116. " last_out_time INTEGER NOT NULL DEFAULT 0,"
  117. " current_mode INTEGER NOT NULL DEFAULT 0" // ✅ v43修复:DEFAULT 0 = INBOUND_ALLOWED
  118. ");"
  119. "CREATE INDEX IF NOT EXISTS idx_slc_plate ON station_lock_cache(plate_number);";
  120. rc = sqlite3_exec(g_db, create_station_lock_sql, nullptr, nullptr, &errMsg);
  121. if (rc != SQLITE_OK) {
  122. std::cerr << "创建station_lock_cache表失败: " << errMsg << std::endl;
  123. sqlite3_free(errMsg);
  124. } else {
  125. std::cout << "✅ station_lock_cache表初始化成功" << std::endl;
  126. }
  127. // ✅ fix24-v9新增:称重记录表
  128. const char* create_weight_sql =
  129. "CREATE TABLE IF NOT EXISTS weight_records ("
  130. " id INTEGER PRIMARY KEY AUTOINCREMENT,"
  131. " create_time DATETIME DEFAULT (datetime('now', 'localtime')),"
  132. " plate_number TEXT NOT NULL,"
  133. " tb_num TEXT NOT NULL,"
  134. " station_type INTEGER NOT NULL,"
  135. " weight_kg REAL NOT NULL,"
  136. " upload_status INTEGER DEFAULT 0,"
  137. " retry_count INTEGER DEFAULT 0,"
  138. " response_msg TEXT"
  139. ");"
  140. "CREATE INDEX IF NOT EXISTS idx_wr_plate ON weight_records(plate_number);"
  141. "CREATE INDEX IF NOT EXISTS idx_wr_tb_num ON weight_records(tb_num);"
  142. "CREATE INDEX IF NOT EXISTS idx_wr_status ON weight_records(upload_status);";
  143. rc = sqlite3_exec(g_db, create_weight_sql, nullptr, nullptr, &errMsg);
  144. if (rc != SQLITE_OK) {
  145. std::cerr << "创建weight_records表失败: " << errMsg << std::endl;
  146. sqlite3_free(errMsg);
  147. } else {
  148. std::cout << "✅ weight_records表初始化成功" << std::endl;
  149. }
  150. rc = sqlite3_exec(g_db, create_index_sql, nullptr, nullptr, &errMsg);
  151. if (rc != SQLITE_OK) {
  152. sqlite3_free(errMsg);
  153. }
  154. std::cout << "✅ 数据库初始化成功: " << g_db_path << std::endl;
  155. return 0;
  156. }
  157. // ✅ 优化N+1查询为GROUP BY聚合查询,启动速度提升100倍以上
  158. void load_recent_tbnums_from_db()
  159. {
  160. if (!g_db) return;
  161. int window_seconds = g_time_window_min * 60;
  162. time_t now = time(NULL);
  163. std::vector<std::tuple<std::string, std::string, int, time_t, int>> records;
  164. {
  165. std::lock_guard<std::mutex> lock(g_db_mtx);
  166. const char* sql =
  167. "SELECT ur1.plate_number, ur1.tb_num, ur1.station_type, ur1.create_time, "
  168. "COUNT(ur2.id) as photo_count "
  169. "FROM upload_records ur1 "
  170. "LEFT JOIN upload_records ur2 ON "
  171. "ur2.plate_number = ur1.plate_number "
  172. "AND ur2.station_type = ur1.station_type "
  173. "AND ur2.create_time >= ur1.create_time "
  174. "AND ur2.lo_upload_status = 1 "
  175. "AND ur2.hi_upload_status = 1 "
  176. "GROUP BY ur1.plate_number, ur1.station_type, ur1.create_time, ur1.tb_num "
  177. "ORDER BY ur1.create_time DESC LIMIT 1000;";
  178. sqlite3_stmt* stmt = nullptr;
  179. int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
  180. if (rc != SQLITE_OK) {
  181. std::cerr << "准备恢复缓存语句失败: " << sqlite3_errmsg(g_db) << std::endl;
  182. return;
  183. }
  184. while (sqlite3_step(stmt) == SQLITE_ROW) {
  185. const unsigned char* plate_text = sqlite3_column_text(stmt, 0);
  186. const unsigned char* tb_text = sqlite3_column_text(stmt, 1);
  187. int station_type = sqlite3_column_int(stmt, 2);
  188. const unsigned char* create_time_text = sqlite3_column_text(stmt, 3);
  189. int photo_count = sqlite3_column_int(stmt, 4);
  190. if (!plate_text || !tb_text || !create_time_text) continue;
  191. std::string plate = reinterpret_cast<const char*>(plate_text);
  192. std::string tb = reinterpret_cast<const char*>(tb_text);
  193. std::string create_time_str = reinterpret_cast<const char*>(create_time_text);
  194. struct tm tm_buf;
  195. memset(&tm_buf, 0, sizeof(tm_buf));
  196. if (strptime(create_time_str.c_str(), "%Y-%m-%d %H:%M:%S", &tm_buf) == NULL) continue;
  197. time_t ct = mktime(&tm_buf);
  198. if (ct == (time_t)-1) continue;
  199. if (difftime(now, ct) <= window_seconds) {
  200. records.emplace_back(plate, tb, station_type, ct, photo_count);
  201. }
  202. else {
  203. break;
  204. }
  205. }
  206. sqlite3_finalize(stmt);
  207. }
  208. int restored_count = 0;
  209. for (const auto& rec : records) {
  210. const std::string& plate = std::get<0>(rec);
  211. const std::string& tb = std::get<1>(rec);
  212. int station_type = std::get<2>(rec);
  213. time_t ct = std::get<3>(rec);
  214. int photo_count = std::get<4>(rec);
  215. if (station_type == 1) {
  216. std::lock_guard<std::mutex> lock2(g_in_bill_cache_mtx);
  217. if (g_in_bill_cache.find(plate) == g_in_bill_cache.end()) {
  218. g_in_bill_cache[plate] = { tb, ct, 0, photo_count, true };
  219. restored_count++;
  220. }
  221. }
  222. else if (station_type == 2) {
  223. std::lock_guard<std::mutex> lock2(g_out_bill_cache_mtx);
  224. if (g_out_bill_cache.find(plate) == g_out_bill_cache.end()) {
  225. g_out_bill_cache[plate] = { tb, ct, 0, photo_count, true };
  226. restored_count++;
  227. }
  228. }
  229. }
  230. std::cout << "✅ 从数据库恢复了 " << restored_count << " 条联单缓存" << std::endl;
  231. }
  232. void close_database() {
  233. std::lock_guard<std::mutex> lock(g_db_mtx);
  234. if (g_db) {
  235. sqlite3_close(g_db);
  236. g_db = nullptr;
  237. }
  238. }
  239. int db_self_test() {
  240. std::lock_guard<std::mutex> lock(g_db_mtx);
  241. if (!g_db) return -1;
  242. const char* insert_sql = "INSERT INTO upload_records (plate_number, tb_num, station_type) "
  243. "VALUES ('__db_test__', 'TESTTB', 1);";
  244. char* err = nullptr;
  245. int rc = sqlite3_exec(g_db, insert_sql, nullptr, nullptr, &err);
  246. if (rc != SQLITE_OK) {
  247. std::cerr << "数据库自测失败: 插入失败" << std::endl;
  248. if (err) sqlite3_free(err);
  249. return -1;
  250. }
  251. const char* select_sql = "SELECT id FROM upload_records WHERE plate_number='__db_test__' LIMIT 1;";
  252. sqlite3_stmt* stmt = nullptr;
  253. rc = sqlite3_prepare_v2(g_db, select_sql, -1, &stmt, nullptr);
  254. if (rc != SQLITE_OK || sqlite3_step(stmt) != SQLITE_ROW) {
  255. std::cerr << "数据库自测失败: 查询失败" << std::endl;
  256. sqlite3_finalize(stmt);
  257. return -1;
  258. }
  259. int test_id = sqlite3_column_int(stmt, 0);
  260. sqlite3_finalize(stmt);
  261. char delete_sql[256];
  262. snprintf(delete_sql, sizeof(delete_sql), "DELETE FROM upload_records WHERE id=%d;", test_id);
  263. rc = sqlite3_exec(g_db, delete_sql, nullptr, nullptr, &err);
  264. if (rc != SQLITE_OK) {
  265. if (err) sqlite3_free(err);
  266. }
  267. std::cout << "✅ 数据库自测通过" << std::endl;
  268. return 0;
  269. }
  270. void db_cleanup_old_records_unlocked() {
  271. if (!g_db) return;
  272. struct stat st;
  273. if (stat(g_db_path.c_str(), &st) == -1) {
  274. std::cerr << "[数据库清理] stat调用失败: " << strerror(errno)
  275. << ",跳过本次清理" << std::endl;
  276. return;
  277. }
  278. if (st.st_size < DB_CLEANUP_THRESHOLD_BYTES) {
  279. return;
  280. }
  281. char* err_msg = nullptr;
  282. const char* delete_sql = "DELETE FROM upload_records WHERE create_time < datetime('now', '-30 days');";
  283. int rc = sqlite3_exec(g_db, delete_sql, nullptr, nullptr, &err_msg);
  284. if (rc != SQLITE_OK) {
  285. std::cerr << "[数据库清理] 删除旧记录失败: " << err_msg << std::endl;
  286. sqlite3_free(err_msg);
  287. } else {
  288. int affected = sqlite3_changes(g_db);
  289. std::cout << "[数据库清理] 删除30天前记录 " << affected << " 条" << std::endl;
  290. }
  291. }
  292. // ============================================
  293. // ============================================
  294. // TIME_WINDOW持久化函数
  295. // ============================================
  296. // 保存单个车牌的时间窗口缓存到数据库(仅保存到数据库,不更新内存缓存)
  297. void db_save_tw_cache(const std::string& plate, bool is_in, time_t create_time, time_t last_time, int bill_count, int photo_count, const std::string& tb_num, int generation) {
  298. std::lock_guard<std::mutex> lock(g_db_mtx);
  299. if (!g_db) return;
  300. char* errMsg = nullptr;
  301. std::string sql = "INSERT OR REPLACE INTO time_window_cache (plate_number, station_type, create_time, last_time, bill_count, photo_count, tb_num, generation) VALUES (?, ?, ?, ?, ?, ?, ?, ?);";
  302. sqlite3_stmt* stmt;
  303. if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) {
  304. sqlite3_bind_text(stmt, 1, plate.c_str(), -1, SQLITE_TRANSIENT);
  305. sqlite3_bind_int(stmt, 2, is_in ? 0 : 1); // 0=进站, 1=出站
  306. sqlite3_bind_int64(stmt, 3, create_time);
  307. sqlite3_bind_int64(stmt, 4, last_time);
  308. sqlite3_bind_int(stmt, 5, bill_count);
  309. sqlite3_bind_int(stmt, 6, photo_count);
  310. sqlite3_bind_text(stmt, 7, tb_num.empty() ? "" : tb_num.c_str(), -1, SQLITE_TRANSIENT);
  311. sqlite3_bind_int(stmt, 8, generation);
  312. sqlite3_step(stmt);
  313. sqlite3_finalize(stmt);
  314. }
  315. // ✅ 修复:不再在此处更新内存缓存,避免死锁
  316. // 内存缓存的更新由调用方在持有正确锁的情况下完成
  317. }
  318. // 辅助函数:将单条TIME_WINDOW缓存记录加载到内存
  319. static void _load_single_tw_cache_record(const std::string& plate, int station_type,
  320. time_t create_time, time_t last_time, int bill_count, int photo_count,
  321. const std::string& tb_num, int generation) {
  322. bool is_in = (station_type == 0);
  323. if (is_in) {
  324. std::lock_guard<std::mutex> lock_in(g_in_bill_cache_mtx);
  325. g_in_bill_cache[plate] = { tb_num, create_time, 0, photo_count, (bill_count > 0) };
  326. } else {
  327. std::lock_guard<std::mutex> lock_out(g_out_bill_cache_mtx);
  328. g_out_bill_cache[plate] = { tb_num, create_time, 0, photo_count, (bill_count > 0) };
  329. }
  330. std::lock_guard<std::mutex> lock_rec(is_in ? g_in_record_mtx : g_out_record_mtx);
  331. auto& records = is_in ? g_in_upload_records : g_out_upload_records;
  332. SceneUploadRecord rec;
  333. rec.bill_count = bill_count;
  334. rec.last_time = last_time;
  335. rec.create_time = create_time;
  336. rec.tb_num = tb_num;
  337. rec.generation = generation;
  338. rec.bill_created = (bill_count > 0);
  339. records[plate] = rec;
  340. }
  341. // 加载时间窗口缓存到内存(内部版本,无锁,由db_load_tw_cache调用)
  342. static void _db_load_tw_cache_unlocked() {
  343. if (!g_db) return;
  344. time_t now = time(NULL);
  345. int window_seconds = g_time_window_min * 60;
  346. std::cout << "[DEBUG] 当前时间: " << now << ", TIME_WINDOW: " << g_time_window_min << "分钟(" << window_seconds << "秒)" << std::endl;
  347. // 从time_window_cache加载
  348. const char* sql = "SELECT plate_number, station_type, create_time, last_time, bill_count, photo_count, tb_num, generation FROM time_window_cache;";
  349. sqlite3_stmt* stmt;
  350. int count = 0;
  351. int reset_count = 0;
  352. if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
  353. while (sqlite3_step(stmt) == SQLITE_ROW) {
  354. std::string plate = (const char*)sqlite3_column_text(stmt, 0);
  355. int station_type = sqlite3_column_int(stmt, 1);
  356. bool is_in = (station_type == 0);
  357. time_t create_time = sqlite3_column_int64(stmt, 2);
  358. time_t last_time = sqlite3_column_int64(stmt, 3);
  359. int bill_count = sqlite3_column_int(stmt, 4);
  360. int photo_count = sqlite3_column_int(stmt, 5);
  361. const char* tb_num_cstr = (const char*)sqlite3_column_text(stmt, 6);
  362. std::string tb_num = tb_num_cstr ? tb_num_cstr : "";
  363. int generation = sqlite3_column_int(stmt, 7);
  364. // ✅ 核心判断:用last_time判断是否超过TIME_WINDOW
  365. // 如果last_time=0,用create_time
  366. time_t record_time = last_time > 0 ? last_time : create_time;
  367. // 如果record_time=0,说明是新记录(从未处理过),正常加载
  368. if (record_time == 0) {
  369. std::cout << "[DEBUG] " << plate << (is_in ? "进站" : "出站")
  370. << " 新记录,首次处理,正常加载" << std::endl;
  371. // 正常加载到内存
  372. _load_single_tw_cache_record(plate, station_type, create_time, last_time,
  373. bill_count, photo_count, tb_num, generation);
  374. count++;
  375. } else {
  376. std::cout << "[DEBUG] " << plate << (is_in ? "进站" : "出站")
  377. << " last_time=" << last_time << ", create_time=" << create_time
  378. << ", 距今: " << (int)difftime(now, record_time) << "秒" << std::endl;
  379. // ✅ 判断是否超过TIME_WINDOW
  380. if (difftime(now, record_time) >= window_seconds) {
  381. std::cout << "[TIME_WINDOW重置] " << plate << (is_in ? "进站" : "出站")
  382. << " 距上次处理" << (int)difftime(now, record_time) << "秒超过" << window_seconds << "秒,重启后允许重置" << std::endl;
  383. // ✅ v43.2修复:使用参数化查询防止SQL注入
  384. const char* del_sql = "DELETE FROM time_window_cache WHERE plate_number=? AND station_type=?;";
  385. sqlite3_stmt* del_stmt;
  386. if (sqlite3_prepare_v2(g_db, del_sql, -1, &del_stmt, nullptr) == SQLITE_OK) {
  387. sqlite3_bind_text(del_stmt, 1, plate.c_str(), -1, SQLITE_TRANSIENT);
  388. sqlite3_bind_int(del_stmt, 2, station_type);
  389. sqlite3_step(del_stmt);
  390. sqlite3_finalize(del_stmt);
  391. }
  392. // ✅ 关键修复:同时清理 g_upload_records 中的旧记录,避免重启后配额检查错误
  393. if (is_in) {
  394. std::lock_guard<std::mutex> lock(g_in_record_mtx);
  395. g_in_upload_records.erase(plate);
  396. } else {
  397. std::lock_guard<std::mutex> lock(g_out_record_mtx);
  398. g_out_upload_records.erase(plate);
  399. }
  400. reset_count++;
  401. // 不加载到内存,让程序重新识别
  402. } else {
  403. // ✅ 5分钟内,正常加载缓存
  404. std::cout << "[DEBUG] " << plate << (is_in ? "进站" : "出站")
  405. << " 5分钟内,正常加载缓存" << std::endl;
  406. _load_single_tw_cache_record(plate, station_type, create_time, last_time,
  407. bill_count, photo_count, tb_num, generation);
  408. count++;
  409. }
  410. }
  411. }
  412. sqlite3_finalize(stmt);
  413. std::cout << "✅ 从数据库加载" << count << "条TIME_WINDOW缓存记录,重置" << reset_count << "条" << std::endl;
  414. }
  415. }
  416. // 加载时间窗口缓存到内存(带锁版本,供外部调用)
  417. void db_load_tw_cache() {
  418. std::lock_guard<std::mutex> lock(g_db_mtx);
  419. _db_load_tw_cache_unlocked();
  420. }
  421. // 清理过期的时间窗口缓存(超过24小时)
  422. void db_cleanup_expired_tw_cache() {
  423. std::lock_guard<std::mutex> lock(g_db_mtx);
  424. if (!g_db) return;
  425. time_t now = time(NULL);
  426. int expired_time = now - 24 * 60 * 60; // 24小时前
  427. // ✅ Bug修复:使用 last_time 判断过期,因为 create_time 可能为0
  428. // 同时排除 last_time=0 的记录(表示从未处理过)
  429. char sql[256];
  430. snprintf(sql, sizeof(sql),
  431. "DELETE FROM time_window_cache WHERE last_time > 0 AND last_time < %ld;",
  432. expired_time);
  433. char* errMsg = nullptr;
  434. int rc = sqlite3_exec(g_db, sql, nullptr, nullptr, &errMsg);
  435. if (rc == SQLITE_OK) {
  436. int changes = sqlite3_changes(g_db);
  437. if (changes > 0) {
  438. std::cout << "[TIME_WINDOW清理] 删除" << changes << "条过期缓存记录" << std::endl;
  439. }
  440. } else {
  441. std::cerr << "[TIME_WINDOW清理] 删除失败: " << errMsg << std::endl;
  442. sqlite3_free(errMsg);
  443. }
  444. }
  445. void db_vacuum() {
  446. std::lock_guard<std::mutex> lock(g_db_mtx);
  447. if (!g_db) return;
  448. char* err_msg = nullptr;
  449. const char* vacuum_sql = "VACUUM;";
  450. int rc = sqlite3_exec(g_db, vacuum_sql, nullptr, nullptr, &err_msg);
  451. if (rc != SQLITE_OK) {
  452. std::cerr << "[数据库优化] VACUUM失败: " << err_msg << std::endl;
  453. sqlite3_free(err_msg);
  454. } else {
  455. std::cout << "[数据库优化] VACUUM执行完成,释放磁盘空间" << std::endl;
  456. }
  457. }
  458. void db_cleanup_old_records() {
  459. std::lock_guard<std::mutex> lock(g_db_mtx);
  460. db_cleanup_old_records_unlocked();
  461. }
  462. // ✅ v41 优化:单次数据库操作最大阻塞时间 < 100ms
  463. int db_insert_record_with_retry(const std::string& plate_number, const std::string& tb_num, int station_type,
  464. const std::string& lo_photo_path, const std::string& hi_photo_path,
  465. int lo_upload_status, int hi_upload_status, int feishu_status,
  466. time_t capture_time) { // ✅ fix24-v13: 新增capture_time参数
  467. for (int retry = 0; retry < DB_INSERT_RETRY_COUNT; retry++) {
  468. if (retry > 0) {
  469. // 仅重试1次,等待50ms
  470. std::cerr << "[数据库插入] 重试第" << retry << "次,等待50ms..." << std::endl;
  471. std::this_thread::sleep_for(std::chrono::milliseconds(50));
  472. }
  473. int result = db_insert_record(plate_number, tb_num, station_type,
  474. lo_photo_path, hi_photo_path, lo_upload_status, hi_upload_status, feishu_status, capture_time);
  475. if (result > 0) {
  476. if (retry > 0) {
  477. g_metrics.record_db_insert_retry_success();
  478. }
  479. return result;
  480. }
  481. }
  482. g_metrics.record_db_insert_error();
  483. // ✅ v41 新增:数据库插入失败时记录严重告警日志
  484. std::cerr << "[严重] 数据库插入失败,车牌=" << plate_number
  485. << ",联单=" << tb_num << ",数据将在下次重试时重新插入" << std::endl;
  486. return -1;
  487. }
  488. int db_insert_record(const std::string& plate_number, const std::string& tb_num, int station_type,
  489. const std::string& lo_photo_path, const std::string& hi_photo_path,
  490. int lo_upload_status, int hi_upload_status, int feishu_status,
  491. time_t capture_time) { // ✅ fix24-v13: 新增capture_time参数
  492. std::lock_guard<std::mutex> lock(g_db_mtx);
  493. if (!g_db) return -1;
  494. const char* sql = "INSERT INTO upload_records "
  495. "(plate_number, tb_num, station_type, lo_photo_path, hi_photo_path, "
  496. "lo_upload_status, hi_upload_status, feishu_status, capture_time, create_time) "
  497. "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime(?, 'unixepoch', 'localtime'));";
  498. sqlite3_stmt* stmt = nullptr;
  499. int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
  500. if (rc != SQLITE_OK) {
  501. std::cerr << "准备插入语句失败: " << sqlite3_errmsg(g_db) << std::endl;
  502. return -1;
  503. }
  504. sqlite3_bind_text(stmt, 1, plate_number.c_str(), -1, SQLITE_TRANSIENT);
  505. sqlite3_bind_text(stmt, 2, tb_num.c_str(), -1, SQLITE_TRANSIENT);
  506. sqlite3_bind_int(stmt, 3, station_type);
  507. sqlite3_bind_text(stmt, 4, lo_photo_path.empty() ? nullptr : lo_photo_path.c_str(), -1, SQLITE_TRANSIENT);
  508. sqlite3_bind_text(stmt, 5, hi_photo_path.empty() ? nullptr : hi_photo_path.c_str(), -1, SQLITE_TRANSIENT);
  509. sqlite3_bind_int(stmt, 6, lo_upload_status);
  510. sqlite3_bind_int(stmt, 7, hi_upload_status);
  511. sqlite3_bind_int(stmt, 8, feishu_status);
  512. sqlite3_bind_int64(stmt, 9, capture_time > 0 ? capture_time : time(NULL)); // ✅ fix24-v13
  513. sqlite3_bind_int64(stmt, 10, capture_time > 0 ? capture_time : time(NULL)); // ✅ fix24-v17: create_time使用capture_time,避免DB插入延迟
  514. rc = sqlite3_step(stmt);
  515. if (rc != SQLITE_DONE) {
  516. std::cerr << "插入记录失败: " << sqlite3_errmsg(g_db) << std::endl;
  517. sqlite3_finalize(stmt);
  518. return -1;
  519. }
  520. int new_id = static_cast<int>(sqlite3_last_insert_rowid(g_db));
  521. sqlite3_finalize(stmt);
  522. db_cleanup_old_records_unlocked();
  523. if (DEBUG_LOG) {
  524. std::cout << "[DEBUG] 数据库插入成功,ID: " << new_id
  525. << ",车牌: " << plate_number << ",联单: " << tb_num << std::endl;
  526. }
  527. return new_id;
  528. }
  529. int db_update_upload_status(int id, int lo_status, int hi_status) {
  530. std::lock_guard<std::mutex> lock(g_db_mtx);
  531. if (!g_db || id <= 0) return -1;
  532. const char* sql = "UPDATE upload_records SET lo_upload_status=?, hi_upload_status=? WHERE id=?;";
  533. sqlite3_stmt* stmt = nullptr;
  534. int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
  535. if (rc != SQLITE_OK) return -1;
  536. sqlite3_bind_int(stmt, 1, lo_status);
  537. sqlite3_bind_int(stmt, 2, hi_status);
  538. sqlite3_bind_int(stmt, 3, id);
  539. rc = sqlite3_step(stmt);
  540. sqlite3_finalize(stmt);
  541. return (rc == SQLITE_DONE) ? 0 : -1;
  542. }
  543. int db_update_feishu_status(int id, int status) {
  544. std::lock_guard<std::mutex> lock(g_db_mtx);
  545. if (!g_db || id <= 0) return -1;
  546. const char* sql = "UPDATE upload_records SET feishu_status=? WHERE id=?;";
  547. sqlite3_stmt* stmt = nullptr;
  548. int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
  549. if (rc != SQLITE_OK) return -1;
  550. sqlite3_bind_int(stmt, 1, status);
  551. sqlite3_bind_int(stmt, 2, id);
  552. rc = sqlite3_step(stmt);
  553. sqlite3_finalize(stmt);
  554. return (rc == SQLITE_DONE) ? 0 : -1;
  555. }
  556. int db_increment_retry_count(int id) {
  557. std::lock_guard<std::mutex> lock(g_db_mtx);
  558. if (!g_db || id <= 0) return -1;
  559. const char* sql = "UPDATE upload_records SET retry_count=retry_count+1 WHERE id=?;";
  560. sqlite3_stmt* stmt = nullptr;
  561. int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
  562. if (rc != SQLITE_OK) return -1;
  563. sqlite3_bind_int(stmt, 1, id);
  564. rc = sqlite3_step(stmt);
  565. sqlite3_finalize(stmt);
  566. return (rc == SQLITE_DONE) ? 0 : -1;
  567. }
  568. int db_get_total_count() {
  569. std::lock_guard<std::mutex> lock(g_db_mtx);
  570. if (!g_db) return 0;
  571. const char* sql = "SELECT COUNT(*) FROM upload_records;";
  572. sqlite3_stmt* stmt = nullptr;
  573. int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
  574. if (rc != SQLITE_OK) return 0;
  575. int count = 0;
  576. if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
  577. sqlite3_finalize(stmt);
  578. return count;
  579. }
  580. int db_get_failed_count() {
  581. std::lock_guard<std::mutex> lock(g_db_mtx);
  582. if (!g_db) return 0;
  583. const char* sql = "SELECT COUNT(*) FROM upload_records WHERE "
  584. "(lo_upload_status != 1 OR hi_upload_status != 1 OR feishu_status = 2);";
  585. sqlite3_stmt* stmt = nullptr;
  586. int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
  587. if (rc != SQLITE_OK) return 0;
  588. int count = 0;
  589. if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
  590. sqlite3_finalize(stmt);
  591. return count;
  592. }
  593. std::vector<DbRecord> db_get_records(int page, int page_size, const std::string& keyword) {
  594. std::vector<DbRecord> records;
  595. std::lock_guard<std::mutex> lock(g_db_mtx);
  596. if (!g_db) return records;
  597. // ✅ fix24-v14: 校正strftime('%s')的时区问题
  598. // SQLite的strftime('%s', str)将str视为UTC,但create_time存的是本地时间
  599. // 需要减去时区偏移才能得到正确的Unix时间戳
  600. // ✅ fix24-v15.1: 显式CAST为INTEGER,解决COALESCE类型不一致导致排序按字符串比较的Bug
  601. std::string tz_modifier = std::to_string(-g_tz_offset_seconds) + " seconds";
  602. std::string sort_expr = "CAST(COALESCE(capture_time, strftime('%s', create_time, '" + tz_modifier + "')) AS INTEGER)";
  603. std::string sql;
  604. if (keyword.empty()) {
  605. sql = "SELECT id, create_time, capture_time, plate_number, tb_num, station_type, "
  606. "lo_photo_path, hi_photo_path, lo_upload_status, hi_upload_status, "
  607. "feishu_status, retry_count FROM upload_records "
  608. "ORDER BY " + sort_expr + " DESC LIMIT ? OFFSET ?;";
  609. }
  610. else {
  611. sql = "SELECT id, create_time, capture_time, plate_number, tb_num, station_type, "
  612. "lo_photo_path, hi_photo_path, lo_upload_status, hi_upload_status, "
  613. "feishu_status, retry_count FROM upload_records "
  614. "WHERE plate_number LIKE ? OR tb_num LIKE ? OR create_time LIKE ? "
  615. "ORDER BY " + sort_expr + " DESC LIMIT ? OFFSET ?;";
  616. }
  617. sqlite3_stmt* stmt = nullptr;
  618. int rc = sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr);
  619. if (rc != SQLITE_OK) return records;
  620. int param_idx = 1;
  621. if (!keyword.empty()) {
  622. std::string like_keyword = "%" + keyword + "%";
  623. sqlite3_bind_text(stmt, param_idx++, like_keyword.c_str(), -1, SQLITE_TRANSIENT);
  624. sqlite3_bind_text(stmt, param_idx++, like_keyword.c_str(), -1, SQLITE_TRANSIENT);
  625. sqlite3_bind_text(stmt, param_idx++, like_keyword.c_str(), -1, SQLITE_TRANSIENT);
  626. }
  627. sqlite3_bind_int(stmt, param_idx++, page_size);
  628. sqlite3_bind_int(stmt, param_idx++, (page - 1) * page_size);
  629. while (sqlite3_step(stmt) == SQLITE_ROW) {
  630. DbRecord rec;
  631. rec.id = sqlite3_column_int(stmt, 0);
  632. rec.create_time = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
  633. // ✅ fix24-v13: 读取capture_time,旧记录为NULL时fallback到create_time
  634. if (sqlite3_column_type(stmt, 2) != SQLITE_NULL) {
  635. rec.capture_time = sqlite3_column_int64(stmt, 2);
  636. } else {
  637. rec.capture_time = 0;
  638. }
  639. rec.plate_number = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
  640. rec.tb_num = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4));
  641. rec.station_type = sqlite3_column_int(stmt, 5);
  642. const char* lo_path = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6));
  643. rec.lo_photo_path = lo_path ? lo_path : "";
  644. const char* hi_path = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 7));
  645. rec.hi_photo_path = hi_path ? hi_path : "";
  646. rec.lo_upload_status = sqlite3_column_int(stmt, 8);
  647. rec.hi_upload_status = sqlite3_column_int(stmt, 9);
  648. rec.feishu_status = sqlite3_column_int(stmt, 10);
  649. rec.retry_count = sqlite3_column_int(stmt, 11);
  650. records.push_back(rec);
  651. }
  652. sqlite3_finalize(stmt);
  653. return records;
  654. }
  655. // ==================== fix24 v41: 分类查询实现 ====================
  656. // 内部:构造 WHERE 子句(不含 WHERE 关键字本身),并返回绑定参数数量
  657. // 通过 vector<string> 收集绑定值
  658. static std::string _build_where_clause(const RecordQuery& q, std::vector<std::string>& bind_vals) {
  659. std::string where;
  660. auto addCond = [&](const std::string& cond) {
  661. if (where.empty()) where = " WHERE " + cond;
  662. else where += " AND " + cond;
  663. };
  664. if (!q.plate_number.empty()) {
  665. addCond("plate_number LIKE ?");
  666. bind_vals.push_back("%" + q.plate_number + "%");
  667. }
  668. if (!q.tb_num.empty()) {
  669. addCond("tb_num LIKE ?");
  670. bind_vals.push_back("%" + q.tb_num + "%");
  671. }
  672. if (!q.date_from.empty()) {
  673. addCond("create_time >= ?");
  674. bind_vals.push_back(q.date_from + " 00:00:00");
  675. }
  676. if (!q.date_to.empty()) {
  677. addCond("create_time <= ?");
  678. bind_vals.push_back(q.date_to + " 23:59:59");
  679. }
  680. if (q.station_type == 1 || q.station_type == 2) {
  681. addCond("station_type = ?");
  682. bind_vals.push_back(std::to_string(q.station_type));
  683. }
  684. if (q.upload_status == 1) {
  685. // 成功:两路均上传成功
  686. addCond("lo_upload_status = 1 AND hi_upload_status = 1");
  687. } else if (q.upload_status == 2) {
  688. // 失败:任一路上传失败
  689. addCond("(lo_upload_status != 1 OR hi_upload_status != 1)");
  690. }
  691. if (q.feishu_status == 1) {
  692. addCond("feishu_status = 1");
  693. } else if (q.feishu_status == 2) {
  694. addCond("feishu_status = 2");
  695. } else if (q.feishu_status == 3) {
  696. addCond("feishu_status = 0");
  697. }
  698. return where;
  699. }
  700. int db_count_records_advanced(const RecordQuery& q) {
  701. std::lock_guard<std::mutex> lock(g_db_mtx);
  702. if (!g_db) return 0;
  703. std::vector<std::string> bind_vals;
  704. std::string where = _build_where_clause(q, bind_vals);
  705. std::string sql = "SELECT COUNT(*) FROM upload_records" + where + ";";
  706. sqlite3_stmt* stmt = nullptr;
  707. if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) return 0;
  708. for (size_t i = 0; i < bind_vals.size(); ++i) {
  709. sqlite3_bind_text(stmt, (int)i + 1, bind_vals[i].c_str(), -1, SQLITE_TRANSIENT);
  710. }
  711. int count = 0;
  712. if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
  713. sqlite3_finalize(stmt);
  714. return count;
  715. }
  716. std::vector<DbRecord> db_get_records_advanced(int page, int page_size, const RecordQuery& q) {
  717. std::vector<DbRecord> records;
  718. std::lock_guard<std::mutex> lock(g_db_mtx);
  719. if (!g_db) return records;
  720. if (page < 1) page = 1;
  721. if (page_size < 1) page_size = 20;
  722. if (page_size > 200) page_size = 200; // 防止过大查询
  723. std::vector<std::string> bind_vals;
  724. std::string where = _build_where_clause(q, bind_vals);
  725. // 排序表达式与旧版保持一致
  726. std::string tz_modifier = std::to_string(-g_tz_offset_seconds) + " seconds";
  727. std::string sort_expr = "CAST(COALESCE(capture_time, strftime('%s', create_time, '" + tz_modifier + "')) AS INTEGER)";
  728. std::string sql =
  729. "SELECT id, create_time, capture_time, plate_number, tb_num, station_type, "
  730. "lo_photo_path, hi_photo_path, lo_upload_status, hi_upload_status, "
  731. "feishu_status, retry_count FROM upload_records"
  732. + where + " ORDER BY " + sort_expr + " DESC LIMIT ? OFFSET ?;";
  733. sqlite3_stmt* stmt = nullptr;
  734. if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) return records;
  735. int param_idx = 1;
  736. for (auto& v : bind_vals) {
  737. sqlite3_bind_text(stmt, param_idx++, v.c_str(), -1, SQLITE_TRANSIENT);
  738. }
  739. sqlite3_bind_int(stmt, param_idx++, page_size);
  740. sqlite3_bind_int(stmt, param_idx++, (page - 1) * page_size);
  741. while (sqlite3_step(stmt) == SQLITE_ROW) {
  742. DbRecord rec;
  743. rec.id = sqlite3_column_int(stmt, 0);
  744. const char* ct = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
  745. rec.create_time = ct ? ct : "";
  746. if (sqlite3_column_type(stmt, 2) != SQLITE_NULL)
  747. rec.capture_time = sqlite3_column_int64(stmt, 2);
  748. else
  749. rec.capture_time = 0;
  750. const char* pn = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
  751. rec.plate_number = pn ? pn : "";
  752. const char* tb = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4));
  753. rec.tb_num = tb ? tb : "";
  754. rec.station_type = sqlite3_column_int(stmt, 5);
  755. const char* lo = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6));
  756. rec.lo_photo_path = lo ? lo : "";
  757. const char* hi = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 7));
  758. rec.hi_photo_path = hi ? hi : "";
  759. rec.lo_upload_status = sqlite3_column_int(stmt, 8);
  760. rec.hi_upload_status = sqlite3_column_int(stmt, 9);
  761. rec.feishu_status = sqlite3_column_int(stmt, 10);
  762. rec.retry_count = sqlite3_column_int(stmt, 11);
  763. records.push_back(rec);
  764. }
  765. sqlite3_finalize(stmt);
  766. return records;
  767. }
  768. DbRecord db_get_record(int id) {
  769. DbRecord rec;
  770. std::lock_guard<std::mutex> lock(g_db_mtx);
  771. if (!g_db || id <= 0) return rec;
  772. const char* sql = "SELECT id, create_time, capture_time, plate_number, tb_num, station_type, "
  773. "lo_photo_path, hi_photo_path, lo_upload_status, hi_upload_status, "
  774. "feishu_status, retry_count FROM upload_records WHERE id=?;";
  775. sqlite3_stmt* stmt = nullptr;
  776. int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
  777. if (rc != SQLITE_OK) return rec;
  778. sqlite3_bind_int(stmt, 1, id);
  779. if (sqlite3_step(stmt) == SQLITE_ROW) {
  780. rec.id = sqlite3_column_int(stmt, 0);
  781. rec.create_time = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
  782. // ✅ fix24-v13: 读取capture_time
  783. if (sqlite3_column_type(stmt, 2) != SQLITE_NULL) {
  784. rec.capture_time = sqlite3_column_int64(stmt, 2);
  785. } else {
  786. rec.capture_time = 0;
  787. }
  788. rec.plate_number = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
  789. rec.tb_num = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4));
  790. rec.station_type = sqlite3_column_int(stmt, 5);
  791. const char* lo_path = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6));
  792. rec.lo_photo_path = lo_path ? lo_path : "";
  793. const char* hi_path = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 7));
  794. rec.hi_photo_path = hi_path ? hi_path : "";
  795. rec.lo_upload_status = sqlite3_column_int(stmt, 8);
  796. rec.hi_upload_status = sqlite3_column_int(stmt, 9);
  797. rec.feishu_status = sqlite3_column_int(stmt, 10);
  798. rec.retry_count = sqlite3_column_int(stmt, 11);
  799. }
  800. sqlite3_finalize(stmt);
  801. return rec;
  802. }
  803. int manual_retry_record(int id) {
  804. DbRecord rec = db_get_record(id);
  805. if (rec.id == 0) return -1;
  806. std::cout << "[手动重试] ID=" << id << " 车牌=" << rec.plate_number << std::endl;
  807. db_increment_retry_count(id);
  808. return 0;
  809. }
  810. // ==================== v43新增:增强重试功能 ====================
  811. // 重试结果结构体
  812. // v43新增:更新联单编号
  813. void db_update_tb_num(int id, const std::string& tb_num) {
  814. std::lock_guard<std::mutex> lock(g_db_mtx);
  815. if (!g_db || id <= 0) return;
  816. const char* sql = "UPDATE upload_records SET tb_num = ? WHERE id = ?;";
  817. sqlite3_stmt* stmt;
  818. if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
  819. sqlite3_bind_text(stmt, 1, tb_num.c_str(), -1, SQLITE_TRANSIENT);
  820. sqlite3_bind_int(stmt, 2, id);
  821. sqlite3_step(stmt);
  822. sqlite3_finalize(stmt);
  823. }
  824. }
  825. // v43新增:更新单侧上传状态
  826. void db_update_upload_status_side(int id, const std::string& side, int status) {
  827. std::lock_guard<std::mutex> lock(g_db_mtx);
  828. if (!g_db || id <= 0) return;
  829. std::string col = (side == "lo") ? "lo_upload_status" : "hi_upload_status";
  830. std::string sql = "UPDATE upload_records SET " + col + " = ? WHERE id = ?;";
  831. sqlite3_stmt* stmt;
  832. if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) {
  833. sqlite3_bind_int(stmt, 1, status);
  834. sqlite3_bind_int(stmt, 2, id);
  835. sqlite3_step(stmt);
  836. sqlite3_finalize(stmt);
  837. }
  838. }
  839. // v43新增:增强重试记录 — 重新获取联单编号 + 重传失败照片 + 更新DB
  840. // ==================== fix24-v9: 称重记录管理 ====================
  841. int db_insert_weight_record(const std::string& plate_number, const std::string& tb_num,
  842. int station_type, double weight_kg, int upload_status, const std::string& response_msg) {
  843. std::lock_guard<std::mutex> lock(g_db_mtx);
  844. if (!g_db) return -1;
  845. const char* sql = "INSERT INTO weight_records (plate_number, tb_num, station_type, weight_kg, upload_status, response_msg) VALUES (?, ?, ?, ?, ?, ?);";
  846. sqlite3_stmt* stmt;
  847. if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return -1;
  848. sqlite3_bind_text(stmt, 1, plate_number.c_str(), -1, SQLITE_TRANSIENT);
  849. sqlite3_bind_text(stmt, 2, tb_num.c_str(), -1, SQLITE_TRANSIENT);
  850. sqlite3_bind_int(stmt, 3, station_type);
  851. sqlite3_bind_double(stmt, 4, weight_kg);
  852. sqlite3_bind_int(stmt, 5, upload_status);
  853. sqlite3_bind_text(stmt, 6, response_msg.c_str(), -1, SQLITE_TRANSIENT);
  854. int rc = sqlite3_step(stmt);
  855. sqlite3_finalize(stmt);
  856. return (rc == SQLITE_DONE) ? (int)sqlite3_last_insert_rowid(g_db) : -1;
  857. }
  858. int db_update_weight_upload_status(int id, int upload_status, const std::string& response_msg) {
  859. std::lock_guard<std::mutex> lock(g_db_mtx);
  860. if (!g_db || id <= 0) return -1;
  861. const char* sql = "UPDATE weight_records SET upload_status = ?, response_msg = ? WHERE id = ?;";
  862. sqlite3_stmt* stmt;
  863. if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return -1;
  864. sqlite3_bind_int(stmt, 1, upload_status);
  865. sqlite3_bind_text(stmt, 2, response_msg.c_str(), -1, SQLITE_TRANSIENT);
  866. sqlite3_bind_int(stmt, 3, id);
  867. int rc = sqlite3_step(stmt);
  868. sqlite3_finalize(stmt);
  869. return (rc == SQLITE_DONE) ? 0 : -1;
  870. }
  871. int db_increment_weight_retry_count(int id) {
  872. std::lock_guard<std::mutex> lock(g_db_mtx);
  873. if (!g_db || id <= 0) return -1;
  874. const char* sql = "UPDATE weight_records SET retry_count = retry_count + 1 WHERE id = ?;";
  875. sqlite3_stmt* stmt;
  876. if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return -1;
  877. sqlite3_bind_int(stmt, 1, id);
  878. int rc = sqlite3_step(stmt);
  879. sqlite3_finalize(stmt);
  880. return (rc == SQLITE_DONE) ? 0 : -1;
  881. }
  882. std::vector<DbRecord> db_get_weight_records(int page, int page_size, const std::string& keyword) {
  883. std::vector<DbRecord> records;
  884. std::lock_guard<std::mutex> lock(g_db_mtx);
  885. if (!g_db) return records;
  886. if (page < 1) page = 1;
  887. if (page_size < 1 || page_size > 100) page_size = 20;
  888. int offset = (page - 1) * page_size;
  889. std::string sql;
  890. if (keyword.empty()) {
  891. sql = "SELECT id, create_time, plate_number, tb_num, station_type, weight_kg, upload_status, retry_count, response_msg FROM weight_records ORDER BY id DESC LIMIT ? OFFSET ?;";
  892. } else {
  893. sql = "SELECT id, create_time, plate_number, tb_num, station_type, weight_kg, upload_status, retry_count, response_msg FROM weight_records WHERE plate_number LIKE ? OR tb_num LIKE ? ORDER BY id DESC LIMIT ? OFFSET ?;";
  894. }
  895. sqlite3_stmt* stmt;
  896. if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) return records;
  897. if (keyword.empty()) {
  898. sqlite3_bind_int(stmt, 1, page_size);
  899. sqlite3_bind_int(stmt, 2, offset);
  900. } else {
  901. std::string kw = "%" + keyword + "%";
  902. sqlite3_bind_text(stmt, 1, kw.c_str(), -1, SQLITE_TRANSIENT);
  903. sqlite3_bind_text(stmt, 2, kw.c_str(), -1, SQLITE_TRANSIENT);
  904. sqlite3_bind_int(stmt, 3, page_size);
  905. sqlite3_bind_int(stmt, 4, offset);
  906. }
  907. while (sqlite3_step(stmt) == SQLITE_ROW) {
  908. DbRecord rec;
  909. rec.id = sqlite3_column_int(stmt, 0);
  910. rec.create_time = (const char*)sqlite3_column_text(stmt, 1);
  911. rec.plate_number = (const char*)sqlite3_column_text(stmt, 2);
  912. rec.tb_num = (const char*)sqlite3_column_text(stmt, 3);
  913. rec.station_type = sqlite3_column_int(stmt, 4);
  914. rec.lo_upload_status = sqlite3_column_int(stmt, 6); // upload_status
  915. rec.hi_upload_status = sqlite3_column_int(stmt, 7); // retry_count
  916. rec.feishu_status = 0;
  917. double weight = sqlite3_column_double(stmt, 5);
  918. rec.hi_photo_path = std::to_string(weight);
  919. const char* msg = (const char*)sqlite3_column_text(stmt, 8);
  920. rec.lo_photo_path = msg ? msg : "";
  921. records.push_back(rec);
  922. }
  923. sqlite3_finalize(stmt);
  924. return records;
  925. }
  926. int db_get_weight_total_count() {
  927. std::lock_guard<std::mutex> lock(g_db_mtx);
  928. if (!g_db) return 0;
  929. const char* sql = "SELECT COUNT(*) FROM weight_records;";
  930. sqlite3_stmt* stmt;
  931. if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return 0;
  932. int count = 0;
  933. if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
  934. sqlite3_finalize(stmt);
  935. return count;
  936. }
  937. int db_get_weight_failed_count() {
  938. std::lock_guard<std::mutex> lock(g_db_mtx);
  939. if (!g_db) return 0;
  940. const char* sql = "SELECT COUNT(*) FROM weight_records WHERE upload_status = 2;";
  941. sqlite3_stmt* stmt;
  942. if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return 0;
  943. int count = 0;
  944. if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
  945. sqlite3_finalize(stmt);
  946. return count;
  947. }
  948. // fix27: 根据车牌和联单编号获取重量(用于飞书消息)
  949. double db_get_weight_by_plate(const std::string& plate, const std::string& tb_num) {
  950. std::lock_guard<std::mutex> lock(g_db_mtx);
  951. if (!g_db) return -1.0;
  952. const char* sql = "SELECT weight_kg FROM weight_records WHERE plate_number = ? AND tb_num = ? ORDER BY id DESC LIMIT 1;";
  953. sqlite3_stmt* stmt;
  954. if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return -1.0;
  955. sqlite3_bind_text(stmt, 1, plate.c_str(), -1, SQLITE_TRANSIENT);
  956. sqlite3_bind_text(stmt, 2, tb_num.c_str(), -1, SQLITE_TRANSIENT);
  957. double weight = -1.0;
  958. if (sqlite3_step(stmt) == SQLITE_ROW) {
  959. weight = sqlite3_column_double(stmt, 0);
  960. }
  961. sqlite3_finalize(stmt);
  962. return weight;
  963. }
  964. // fix27: 等待重量数据写入数据库(最多等待 max_wait_ms 毫秒)
  965. double db_wait_weight_by_plate(const std::string& plate, const std::string& tb_num, int max_wait_ms) {
  966. // 每 200ms 检查一次,最多等待 max_wait_ms
  967. const int check_interval_ms = 200;
  968. const int max_checks = max_wait_ms / check_interval_ms;
  969. for (int i = 0; i < max_checks; i++) {
  970. double weight = db_get_weight_by_plate(plate, tb_num);
  971. if (weight >= 0) {
  972. return weight; // 找到重量数据
  973. }
  974. std::this_thread::sleep_for(std::chrono::milliseconds(check_interval_ms));
  975. }
  976. return -1.0; // 超时未找到
  977. }
  978. // ==================== fix24 v41: 称重记录分类查询 ====================
  979. // 构建 WHERE 子句,返回参数化占位符;bind_values 收集需要绑定的值
  980. // 返回值: WHERE 子句字符串(不含 "WHERE" 关键字),空串表示无条件
  981. static std::string _build_weight_where_clause(const WeightQuery& q,
  982. std::vector<std::string>& bind_texts, std::vector<int>& bind_ints) {
  983. std::string where;
  984. bind_texts.clear();
  985. bind_ints.clear();
  986. if (!q.plate_number.empty()) {
  987. if (!where.empty()) where += " AND ";
  988. where += "plate_number LIKE ?";
  989. bind_texts.push_back("%" + q.plate_number + "%");
  990. }
  991. if (!q.tb_num.empty()) {
  992. if (!where.empty()) where += " AND ";
  993. where += "tb_num LIKE ?";
  994. bind_texts.push_back("%" + q.tb_num + "%");
  995. }
  996. if (!q.date_from.empty()) {
  997. if (!where.empty()) where += " AND ";
  998. where += "create_time >= ?";
  999. bind_texts.push_back(q.date_from + " 00:00:00");
  1000. }
  1001. if (!q.date_to.empty()) {
  1002. if (!where.empty()) where += " AND ";
  1003. where += "create_time <= ?";
  1004. bind_texts.push_back(q.date_to + " 23:59:59");
  1005. }
  1006. if (q.station_type == 1 || q.station_type == 2) {
  1007. if (!where.empty()) where += " AND ";
  1008. where += "station_type = ?";
  1009. bind_ints.push_back(q.station_type);
  1010. }
  1011. if (q.upload_status == 1) {
  1012. // 成功
  1013. if (!where.empty()) where += " AND ";
  1014. where += "upload_status = 1";
  1015. } else if (q.upload_status == 2) {
  1016. // 失败
  1017. if (!where.empty()) where += " AND ";
  1018. where += "upload_status = 2";
  1019. } else if (q.upload_status == 3) {
  1020. // 待上传
  1021. if (!where.empty()) where += " AND ";
  1022. where += "upload_status = 0";
  1023. }
  1024. return where;
  1025. }
  1026. int db_count_weight_records_advanced(const WeightQuery& q) {
  1027. std::lock_guard<std::mutex> lock(g_db_mtx);
  1028. if (!g_db) return 0;
  1029. std::vector<std::string> bind_texts;
  1030. std::vector<int> bind_ints;
  1031. std::string where = _build_weight_where_clause(q, bind_texts, bind_ints);
  1032. std::string sql = "SELECT COUNT(*) FROM weight_records";
  1033. if (!where.empty()) sql += " WHERE " + where;
  1034. sql += ";";
  1035. sqlite3_stmt* stmt;
  1036. if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) return 0;
  1037. int idx = 1;
  1038. for (auto& v : bind_texts) sqlite3_bind_text(stmt, idx++, v.c_str(), -1, SQLITE_TRANSIENT);
  1039. for (auto& v : bind_ints) sqlite3_bind_int(stmt, idx++, v);
  1040. int count = 0;
  1041. if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
  1042. sqlite3_finalize(stmt);
  1043. return count;
  1044. }
  1045. std::vector<DbRecord> db_get_weight_records_advanced(int page, int page_size, const WeightQuery& q) {
  1046. std::vector<DbRecord> records;
  1047. std::lock_guard<std::mutex> lock(g_db_mtx);
  1048. if (!g_db) return records;
  1049. if (page < 1) page = 1;
  1050. if (page_size < 1) page_size = 20;
  1051. if (page_size > 200) page_size = 200;
  1052. int offset = (page - 1) * page_size;
  1053. std::vector<std::string> bind_texts;
  1054. std::vector<int> bind_ints;
  1055. std::string where = _build_weight_where_clause(q, bind_texts, bind_ints);
  1056. std::string sql = "SELECT id, create_time, plate_number, tb_num, station_type, weight_kg, upload_status, retry_count, response_msg FROM weight_records";
  1057. if (!where.empty()) sql += " WHERE " + where;
  1058. sql += " ORDER BY id DESC LIMIT ? OFFSET ?;";
  1059. sqlite3_stmt* stmt;
  1060. if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) return records;
  1061. int idx = 1;
  1062. for (auto& v : bind_texts) sqlite3_bind_text(stmt, idx++, v.c_str(), -1, SQLITE_TRANSIENT);
  1063. for (auto& v : bind_ints) sqlite3_bind_int(stmt, idx++, v);
  1064. sqlite3_bind_int(stmt, idx++, page_size);
  1065. sqlite3_bind_int(stmt, idx++, offset);
  1066. while (sqlite3_step(stmt) == SQLITE_ROW) {
  1067. DbRecord rec;
  1068. rec.id = sqlite3_column_int(stmt, 0);
  1069. const char* ct = (const char*)sqlite3_column_text(stmt, 1);
  1070. rec.create_time = ct ? ct : "";
  1071. const char* pn = (const char*)sqlite3_column_text(stmt, 2);
  1072. rec.plate_number = pn ? pn : "";
  1073. const char* tn = (const char*)sqlite3_column_text(stmt, 3);
  1074. rec.tb_num = tn ? tn : "";
  1075. rec.station_type = sqlite3_column_int(stmt, 4);
  1076. rec.lo_upload_status = sqlite3_column_int(stmt, 6); // upload_status
  1077. rec.hi_upload_status = sqlite3_column_int(stmt, 7); // retry_count
  1078. rec.feishu_status = 0;
  1079. double weight = sqlite3_column_double(stmt, 5);
  1080. rec.hi_photo_path = std::to_string(weight);
  1081. const char* msg = (const char*)sqlite3_column_text(stmt, 8);
  1082. rec.lo_photo_path = msg ? msg : "";
  1083. records.push_back(rec);
  1084. }
  1085. sqlite3_finalize(stmt);
  1086. return records;
  1087. }
  1088. // ==================== fix24 v41: 照片清理DB同步 ====================
  1089. /**
  1090. * 从完整路径中提取文件名(basename)
  1091. * 例: /opt/openAI/.../PlateJPG/沪FQ7108_1787365677_side_out.jpg -> 沪FQ7108_1787365677_side_out.jpg
  1092. */
  1093. static std::string extract_basename(const std::string& path) {
  1094. size_t pos = path.find_last_of("/\\");
  1095. if (pos == std::string::npos) return path;
  1096. return path.substr(pos + 1);
  1097. }
  1098. /**
  1099. * 清空指定路径对应的照片字段(fix24 v41 修复版)
  1100. * 当 daily_file_cleanup 物理删除照片后调用,同步清空 upload_records 表中的路径字段。
  1101. * 保留业务记录(车牌号、联单编号等),只清空 lo_photo_path/hi_photo_path。
  1102. *
  1103. * 关键修复:DB 中存储的是相对文件名(如 沪FQ7108_xxx.jpg),
  1104. * 而 daily_file_cleanup 传入的是绝对路径(如 /opt/.../PlateJPG/沪FQ7108_xxx.jpg),
  1105. * 因此必须提取 basename 再匹配,否则永远匹配不到。
  1106. *
  1107. * @param paths 被删除的照片完整路径列表(绝对路径或相对文件名均可)
  1108. * @return 成功更新的字段数(lo + hi 合计)
  1109. */
  1110. int db_clear_photo_paths_by_paths(const std::vector<std::string>& paths) {
  1111. if (paths.empty()) return 0;
  1112. std::lock_guard<std::mutex> lock(g_db_mtx);
  1113. if (!g_db) return -1;
  1114. int updated_count = 0;
  1115. // 用一个事务批量执行,提升性能
  1116. sqlite3_exec(g_db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr);
  1117. const char* sql_lo = "UPDATE upload_records SET lo_photo_path = NULL WHERE lo_photo_path = ?;";
  1118. const char* sql_hi = "UPDATE upload_records SET hi_photo_path = NULL WHERE hi_photo_path = ?;";
  1119. for (const auto& path : paths) {
  1120. // 提取文件名进行匹配(DB 存储的是相对文件名)
  1121. std::string basename = extract_basename(path);
  1122. if (basename.empty()) continue;
  1123. // 清空 lo_photo_path
  1124. sqlite3_stmt* stmt = nullptr;
  1125. if (sqlite3_prepare_v2(g_db, sql_lo, -1, &stmt, nullptr) == SQLITE_OK) {
  1126. sqlite3_bind_text(stmt, 1, basename.c_str(), -1, SQLITE_TRANSIENT);
  1127. if (sqlite3_step(stmt) == SQLITE_DONE) {
  1128. updated_count += sqlite3_changes(g_db);
  1129. }
  1130. sqlite3_finalize(stmt);
  1131. }
  1132. // 清空 hi_photo_path
  1133. stmt = nullptr;
  1134. if (sqlite3_prepare_v2(g_db, sql_hi, -1, &stmt, nullptr) == SQLITE_OK) {
  1135. sqlite3_bind_text(stmt, 1, basename.c_str(), -1, SQLITE_TRANSIENT);
  1136. if (sqlite3_step(stmt) == SQLITE_DONE) {
  1137. updated_count += sqlite3_changes(g_db);
  1138. }
  1139. sqlite3_finalize(stmt);
  1140. }
  1141. }
  1142. sqlite3_exec(g_db, "COMMIT;", nullptr, nullptr, nullptr);
  1143. return updated_count;
  1144. }
  1145. /**
  1146. * 启动时清理孤儿照片路径(fix24 v41 新增)
  1147. *
  1148. * 扫描 upload_records 表中所有 lo_photo_path / hi_photo_path 非 NULL 的记录,
  1149. * 检查对应文件是否还存在于 PlateJPG/ 目录下;不存在的清空路径字段。
  1150. * 用于修复历史遗留数据(照片已被旧版本清理但 DB 路径未同步)。
  1151. *
  1152. * @param photo_dir PlateJPG 目录的绝对路径(以 / 结尾)
  1153. * @return 成功清空的字段数
  1154. */
  1155. int db_purge_missing_photo_paths(const std::string& photo_dir) {
  1156. std::lock_guard<std::mutex> lock(g_db_mtx);
  1157. if (!g_db) return -1;
  1158. int purged = 0;
  1159. std::vector<std::pair<int64_t, std::string>> lo_paths, hi_paths;
  1160. // 收集所有非空路径
  1161. const char* select_sql =
  1162. "SELECT id, lo_photo_path, hi_photo_path FROM upload_records "
  1163. "WHERE lo_photo_path IS NOT NULL OR hi_photo_path IS NOT NULL;";
  1164. sqlite3_stmt* stmt = nullptr;
  1165. if (sqlite3_prepare_v2(g_db, select_sql, -1, &stmt, nullptr) != SQLITE_OK) {
  1166. std::cerr << "[DB清理] 查询失败: " << sqlite3_errmsg(g_db) << std::endl;
  1167. return -1;
  1168. }
  1169. while (sqlite3_step(stmt) == SQLITE_ROW) {
  1170. int64_t id = sqlite3_column_int64(stmt, 0);
  1171. const char* lo = (const char*)sqlite3_column_text(stmt, 1);
  1172. const char* hi = (const char*)sqlite3_column_text(stmt, 2);
  1173. if (lo && lo[0]) lo_paths.emplace_back(id, lo);
  1174. if (hi && hi[0]) hi_paths.emplace_back(id, hi);
  1175. }
  1176. sqlite3_finalize(stmt);
  1177. if (lo_paths.empty() && hi_paths.empty()) {
  1178. std::cout << "[DB清理] 无需清理的孤儿路径" << std::endl;
  1179. return 0;
  1180. }
  1181. std::cout << "[DB清理] 检查 " << lo_paths.size() << " 条lo路径 + "
  1182. << hi_paths.size() << " 条hi路径..." << std::endl;
  1183. // 检查 lo_photo_path
  1184. sqlite3_exec(g_db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr);
  1185. const char* update_lo = "UPDATE upload_records SET lo_photo_path = NULL WHERE id = ?;";
  1186. for (const auto& [id, filename] : lo_paths) {
  1187. std::string full = photo_dir + filename;
  1188. struct stat st;
  1189. if (stat(full.c_str(), &st) != 0) {
  1190. // 文件不存在,清空
  1191. sqlite3_stmt* u = nullptr;
  1192. if (sqlite3_prepare_v2(g_db, update_lo, -1, &u, nullptr) == SQLITE_OK) {
  1193. sqlite3_bind_int64(u, 1, id);
  1194. if (sqlite3_step(u) == SQLITE_DONE) purged++;
  1195. sqlite3_finalize(u);
  1196. }
  1197. }
  1198. }
  1199. const char* update_hi = "UPDATE upload_records SET hi_photo_path = NULL WHERE id = ?;";
  1200. for (const auto& [id, filename] : hi_paths) {
  1201. std::string full = photo_dir + filename;
  1202. struct stat st;
  1203. if (stat(full.c_str(), &st) != 0) {
  1204. sqlite3_stmt* u = nullptr;
  1205. if (sqlite3_prepare_v2(g_db, update_hi, -1, &u, nullptr) == SQLITE_OK) {
  1206. sqlite3_bind_int64(u, 1, id);
  1207. if (sqlite3_step(u) == SQLITE_DONE) purged++;
  1208. sqlite3_finalize(u);
  1209. }
  1210. }
  1211. }
  1212. sqlite3_exec(g_db, "COMMIT;", nullptr, nullptr, nullptr);
  1213. std::cout << "[DB清理] 清空 " << purged << " 个孤儿路径字段" << std::endl;
  1214. return purged;
  1215. }
  1216. // ==================== fix24 v43: 原子写入两表 ====================
  1217. int db_insert_event_atomic(
  1218. // upload_records 字段
  1219. const std::string& plate_number, const std::string& tb_num, int station_type,
  1220. const std::string& lo_photo_path, const std::string& hi_photo_path,
  1221. int lo_upload_status, int hi_upload_status, int feishu_status,
  1222. time_t capture_time,
  1223. // weight_records 字段
  1224. double weight_kg, int weight_upload_status, const std::string& weight_response_msg)
  1225. {
  1226. std::lock_guard<std::mutex> lock(g_db_mtx);
  1227. if (!g_db) return -1;
  1228. // 开启事务
  1229. char* errmsg = nullptr;
  1230. int rc = sqlite3_exec(g_db, "BEGIN TRANSACTION;", nullptr, nullptr, &errmsg);
  1231. if (rc != SQLITE_OK) {
  1232. std::cerr << "[v43-DB] BEGIN 失败: " << (errmsg ? errmsg : "unknown") << std::endl;
  1233. if (errmsg) sqlite3_free(errmsg);
  1234. return -1;
  1235. }
  1236. // 写入 upload_records
  1237. const char* sql1 = R"(
  1238. INSERT INTO upload_records
  1239. (plate_number, tb_num, station_type, lo_photo_path, hi_photo_path,
  1240. lo_upload_status, hi_upload_status, feishu_status, capture_time)
  1241. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
  1242. )";
  1243. sqlite3_stmt* stmt1 = nullptr;
  1244. if (sqlite3_prepare_v2(g_db, sql1, -1, &stmt1, nullptr) != SQLITE_OK) {
  1245. std::cerr << "[v43-DB] prepare upload_records 失败: " << sqlite3_errmsg(g_db) << std::endl;
  1246. sqlite3_exec(g_db, "ROLLBACK;", nullptr, nullptr, nullptr);
  1247. return -1;
  1248. }
  1249. sqlite3_bind_text(stmt1, 1, plate_number.c_str(), -1, SQLITE_TRANSIENT);
  1250. sqlite3_bind_text(stmt1, 2, tb_num.c_str(), -1, SQLITE_TRANSIENT);
  1251. sqlite3_bind_int(stmt1, 3, station_type);
  1252. sqlite3_bind_text(stmt1, 4, lo_photo_path.c_str(), -1, SQLITE_TRANSIENT);
  1253. sqlite3_bind_text(stmt1, 5, hi_photo_path.c_str(), -1, SQLITE_TRANSIENT);
  1254. sqlite3_bind_int(stmt1, 6, lo_upload_status);
  1255. sqlite3_bind_int(stmt1, 7, hi_upload_status);
  1256. sqlite3_bind_int(stmt1, 8, feishu_status);
  1257. sqlite3_bind_int64(stmt1, 9, (long long)capture_time);
  1258. rc = sqlite3_step(stmt1);
  1259. sqlite3_finalize(stmt1);
  1260. if (rc != SQLITE_DONE) {
  1261. std::cerr << "[v43-DB] INSERT upload_records 失败: " << sqlite3_errmsg(g_db) << std::endl;
  1262. sqlite3_exec(g_db, "ROLLBACK;", nullptr, nullptr, nullptr);
  1263. return -1;
  1264. }
  1265. // 写入 weight_records
  1266. const char* sql2 = R"(
  1267. INSERT INTO weight_records
  1268. (plate_number, tb_num, station_type, weight_kg, upload_status, response_msg)
  1269. VALUES (?, ?, ?, ?, ?, ?);
  1270. )";
  1271. sqlite3_stmt* stmt2 = nullptr;
  1272. if (sqlite3_prepare_v2(g_db, sql2, -1, &stmt2, nullptr) != SQLITE_OK) {
  1273. std::cerr << "[v43-DB] prepare weight_records 失败: " << sqlite3_errmsg(g_db) << std::endl;
  1274. sqlite3_exec(g_db, "ROLLBACK;", nullptr, nullptr, nullptr);
  1275. return -1;
  1276. }
  1277. sqlite3_bind_text(stmt2, 1, plate_number.c_str(), -1, SQLITE_TRANSIENT);
  1278. sqlite3_bind_text(stmt2, 2, tb_num.c_str(), -1, SQLITE_TRANSIENT);
  1279. sqlite3_bind_int(stmt2, 3, station_type);
  1280. sqlite3_bind_double(stmt2, 4, weight_kg);
  1281. sqlite3_bind_int(stmt2, 5, weight_upload_status);
  1282. sqlite3_bind_text(stmt2, 6, weight_response_msg.c_str(), -1, SQLITE_TRANSIENT);
  1283. rc = sqlite3_step(stmt2);
  1284. sqlite3_finalize(stmt2);
  1285. if (rc != SQLITE_DONE) {
  1286. std::cerr << "[v43-DB] INSERT weight_records 失败: " << sqlite3_errmsg(g_db) << std::endl;
  1287. sqlite3_exec(g_db, "ROLLBACK;", nullptr, nullptr, nullptr);
  1288. return -1;
  1289. }
  1290. // 提交事务
  1291. rc = sqlite3_exec(g_db, "COMMIT;", nullptr, nullptr, &errmsg);
  1292. if (rc != SQLITE_OK) {
  1293. std::cerr << "[v43-DB] COMMIT 失败: " << (errmsg ? errmsg : "unknown") << std::endl;
  1294. if (errmsg) sqlite3_free(errmsg);
  1295. sqlite3_exec(g_db, "ROLLBACK;", nullptr, nullptr, nullptr);
  1296. return -1;
  1297. }
  1298. std::cout << "[v43-DB] 原子写入成功: " << plate_number << " 联单:" << tb_num
  1299. << " 重量:" << weight_kg << "kg" << std::endl;
  1300. return 0;
  1301. }
  1302. // ==================== fix24 v43: 按 tb_num 联合查询 ====================
  1303. std::vector<EventRecord> db_get_event_records_by_tbnum(const std::string& tb_num) {
  1304. std::vector<EventRecord> results;
  1305. std::lock_guard<std::mutex> lock(g_db_mtx);
  1306. if (!g_db) return results;
  1307. const char* sql = R"(
  1308. SELECT
  1309. ur.id AS ur_id,
  1310. ur.plate_number,
  1311. ur.tb_num,
  1312. ur.station_type,
  1313. ur.create_time,
  1314. ur.capture_time,
  1315. ur.lo_photo_path,
  1316. ur.hi_photo_path,
  1317. ur.lo_upload_status,
  1318. ur.hi_upload_status,
  1319. ur.feishu_status,
  1320. wr.id AS wr_id,
  1321. wr.weight_kg,
  1322. wr.upload_status AS weight_upload_status,
  1323. wr.response_msg AS weight_response_msg
  1324. FROM upload_records ur
  1325. LEFT JOIN weight_records wr ON wr.id = (
  1326. SELECT MAX(w2.id) FROM weight_records w2
  1327. WHERE w2.tb_num = ur.tb_num AND w2.plate_number = ur.plate_number
  1328. AND w2.station_type = ur.station_type
  1329. )
  1330. WHERE ur.tb_num LIKE ?
  1331. ORDER BY ur.create_time DESC;
  1332. )";
  1333. sqlite3_stmt* stmt = nullptr;
  1334. if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
  1335. std::cerr << "[v43-DB] prepare 联合查询失败: " << sqlite3_errmsg(g_db) << std::endl;
  1336. return results;
  1337. }
  1338. std::string pattern = "%" + tb_num + "%";
  1339. sqlite3_bind_text(stmt, 1, pattern.c_str(), -1, SQLITE_TRANSIENT);
  1340. while (sqlite3_step(stmt) == SQLITE_ROW) {
  1341. EventRecord rec;
  1342. rec.ur_id = sqlite3_column_int(stmt, 0);
  1343. const char* s;
  1344. s = (const char*)sqlite3_column_text(stmt, 1); if (s) rec.plate_number = s;
  1345. s = (const char*)sqlite3_column_text(stmt, 2); if (s) rec.tb_num = s;
  1346. rec.station_type = sqlite3_column_int(stmt, 3);
  1347. s = (const char*)sqlite3_column_text(stmt, 4); if (s) rec.create_time = s;
  1348. // capture_time 存储为 Unix 时间戳 (INTEGER),需转换为格式化字符串
  1349. if (sqlite3_column_type(stmt, 5) != SQLITE_NULL) {
  1350. int64_t cap_ts = sqlite3_column_int64(stmt, 5);
  1351. struct tm tm_buf;
  1352. time_t t = (time_t)cap_ts;
  1353. localtime_r(&t, &tm_buf);
  1354. char buf[32];
  1355. strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf);
  1356. rec.capture_time = buf;
  1357. }
  1358. s = (const char*)sqlite3_column_text(stmt, 6); if (s) rec.lo_photo_path = s;
  1359. s = (const char*)sqlite3_column_text(stmt, 7); if (s) rec.hi_photo_path = s;
  1360. rec.lo_upload_status = sqlite3_column_int(stmt, 8);
  1361. rec.hi_upload_status = sqlite3_column_int(stmt, 9);
  1362. rec.feishu_status = sqlite3_column_int(stmt, 10);
  1363. rec.wr_id = sqlite3_column_int(stmt, 11);
  1364. rec.weight_kg = sqlite3_column_double(stmt, 12);
  1365. rec.weight_upload_status = sqlite3_column_int(stmt, 13);
  1366. s = (const char*)sqlite3_column_text(stmt, 14); if (s) rec.weight_response_msg = s;
  1367. results.push_back(rec);
  1368. }
  1369. sqlite3_finalize(stmt);
  1370. return results;
  1371. }
  1372. // ==================== fix24 v43 P2: 分页联合查询 + 同步统计 ====================
  1373. std::vector<EventRecord> db_get_event_records_joined(int page, int page_size, const SyncQuery& q) {
  1374. std::vector<EventRecord> results;
  1375. std::lock_guard<std::mutex> lock(g_db_mtx);
  1376. if (!g_db) return results;
  1377. std::string sql = R"(
  1378. SELECT
  1379. ur.id AS ur_id,
  1380. ur.plate_number,
  1381. ur.tb_num,
  1382. ur.station_type,
  1383. ur.create_time,
  1384. ur.capture_time,
  1385. ur.lo_photo_path,
  1386. ur.hi_photo_path,
  1387. ur.lo_upload_status,
  1388. ur.hi_upload_status,
  1389. ur.feishu_status,
  1390. wr.id AS wr_id,
  1391. wr.weight_kg,
  1392. wr.upload_status AS weight_upload_status,
  1393. wr.response_msg AS weight_response_msg
  1394. FROM upload_records ur
  1395. LEFT JOIN weight_records wr ON wr.id = (
  1396. SELECT MAX(w2.id) FROM weight_records w2
  1397. WHERE w2.tb_num = ur.tb_num AND w2.plate_number = ur.plate_number
  1398. AND w2.station_type = ur.station_type
  1399. )
  1400. WHERE 1=1
  1401. )";
  1402. std::vector<std::string> binds;
  1403. if (!q.plate_number.empty()) {
  1404. sql += " AND ur.plate_number LIKE ?";
  1405. binds.push_back("%" + q.plate_number + "%");
  1406. }
  1407. if (!q.tb_num.empty()) {
  1408. sql += " AND ur.tb_num LIKE ?";
  1409. binds.push_back("%" + q.tb_num + "%");
  1410. }
  1411. if (!q.date_from.empty()) {
  1412. sql += " AND ur.create_time >= ?";
  1413. binds.push_back(q.date_from + " 00:00:00");
  1414. }
  1415. if (!q.date_to.empty()) {
  1416. sql += " AND ur.create_time <= ?";
  1417. binds.push_back(q.date_to + " 23:59:59");
  1418. }
  1419. if (q.station_type > 0) {
  1420. sql += " AND ur.station_type = ?";
  1421. binds.push_back(std::to_string(q.station_type));
  1422. }
  1423. if (q.sync_status == 1) {
  1424. sql += " AND wr.id IS NOT NULL";
  1425. } else if (q.sync_status == 2) {
  1426. sql += " AND wr.id IS NULL";
  1427. } else if (q.sync_status == 3) {
  1428. sql += " AND wr.id IS NOT NULL AND wr.upload_status = 2";
  1429. }
  1430. sql += " ORDER BY ur.create_time DESC";
  1431. // 分页
  1432. int offset = (page - 1) * page_size;
  1433. sql += " LIMIT ? OFFSET ?";
  1434. sqlite3_stmt* stmt = nullptr;
  1435. if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
  1436. std::cerr << "[v43-P2] prepare 联合查询失败: " << sqlite3_errmsg(g_db) << std::endl;
  1437. return results;
  1438. }
  1439. int idx = 1;
  1440. for (const auto& b : binds) {
  1441. sqlite3_bind_text(stmt, idx++, b.c_str(), -1, SQLITE_TRANSIENT);
  1442. }
  1443. sqlite3_bind_int(stmt, idx++, page_size);
  1444. sqlite3_bind_int(stmt, idx++, offset);
  1445. while (sqlite3_step(stmt) == SQLITE_ROW) {
  1446. EventRecord rec;
  1447. rec.ur_id = sqlite3_column_int(stmt, 0);
  1448. const char* s;
  1449. s = (const char*)sqlite3_column_text(stmt, 1); if (s) rec.plate_number = s;
  1450. s = (const char*)sqlite3_column_text(stmt, 2); if (s) rec.tb_num = s;
  1451. rec.station_type = sqlite3_column_int(stmt, 3);
  1452. s = (const char*)sqlite3_column_text(stmt, 4); if (s) rec.create_time = s;
  1453. // capture_time 存储为 Unix 时间戳 (INTEGER),需转换为格式化字符串
  1454. if (sqlite3_column_type(stmt, 5) != SQLITE_NULL) {
  1455. int64_t cap_ts = sqlite3_column_int64(stmt, 5);
  1456. struct tm tm_buf;
  1457. time_t t = (time_t)cap_ts;
  1458. localtime_r(&t, &tm_buf);
  1459. char buf[32];
  1460. strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf);
  1461. rec.capture_time = buf;
  1462. }
  1463. s = (const char*)sqlite3_column_text(stmt, 6); if (s) rec.lo_photo_path = s;
  1464. s = (const char*)sqlite3_column_text(stmt, 7); if (s) rec.hi_photo_path = s;
  1465. rec.lo_upload_status = sqlite3_column_int(stmt, 8);
  1466. rec.hi_upload_status = sqlite3_column_int(stmt, 9);
  1467. rec.feishu_status = sqlite3_column_int(stmt, 10);
  1468. rec.wr_id = sqlite3_column_int(stmt, 11);
  1469. rec.weight_kg = sqlite3_column_double(stmt, 12);
  1470. rec.weight_upload_status = sqlite3_column_int(stmt, 13);
  1471. s = (const char*)sqlite3_column_text(stmt, 14); if (s) rec.weight_response_msg = s;
  1472. results.push_back(rec);
  1473. }
  1474. sqlite3_finalize(stmt);
  1475. return results;
  1476. }
  1477. int db_count_event_records_joined(const SyncQuery& q) {
  1478. std::lock_guard<std::mutex> lock(g_db_mtx);
  1479. if (!g_db) return 0;
  1480. std::string sql = R"(
  1481. SELECT COUNT(*)
  1482. FROM upload_records ur
  1483. LEFT JOIN weight_records wr ON wr.id = (
  1484. SELECT MAX(w2.id) FROM weight_records w2
  1485. WHERE w2.tb_num = ur.tb_num AND w2.plate_number = ur.plate_number
  1486. AND w2.station_type = ur.station_type
  1487. )
  1488. WHERE 1=1
  1489. )";
  1490. std::vector<std::string> binds;
  1491. if (!q.plate_number.empty()) {
  1492. sql += " AND ur.plate_number LIKE ?";
  1493. binds.push_back("%" + q.plate_number + "%");
  1494. }
  1495. if (!q.tb_num.empty()) {
  1496. sql += " AND ur.tb_num LIKE ?";
  1497. binds.push_back("%" + q.tb_num + "%");
  1498. }
  1499. if (!q.date_from.empty()) {
  1500. sql += " AND ur.create_time >= ?";
  1501. binds.push_back(q.date_from + " 00:00:00");
  1502. }
  1503. if (!q.date_to.empty()) {
  1504. sql += " AND ur.create_time <= ?";
  1505. binds.push_back(q.date_to + " 23:59:59");
  1506. }
  1507. if (q.station_type > 0) {
  1508. sql += " AND ur.station_type = ?";
  1509. binds.push_back(std::to_string(q.station_type));
  1510. }
  1511. if (q.sync_status == 1) {
  1512. sql += " AND wr.id IS NOT NULL";
  1513. } else if (q.sync_status == 2) {
  1514. sql += " AND wr.id IS NULL";
  1515. } else if (q.sync_status == 3) {
  1516. sql += " AND wr.id IS NOT NULL AND wr.upload_status = 2";
  1517. }
  1518. sqlite3_stmt* stmt = nullptr;
  1519. if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
  1520. std::cerr << "[v43-P2] prepare count 联合查询失败: " << sqlite3_errmsg(g_db) << std::endl;
  1521. return 0;
  1522. }
  1523. int idx = 1;
  1524. for (const auto& b : binds) {
  1525. sqlite3_bind_text(stmt, idx++, b.c_str(), -1, SQLITE_TRANSIENT);
  1526. }
  1527. int count = 0;
  1528. if (sqlite3_step(stmt) == SQLITE_ROW) {
  1529. count = sqlite3_column_int(stmt, 0);
  1530. }
  1531. sqlite3_finalize(stmt);
  1532. return count;
  1533. }
  1534. SyncStats db_get_sync_stats() {
  1535. SyncStats stats;
  1536. std::lock_guard<std::mutex> lock(g_db_mtx);
  1537. if (!g_db) return stats;
  1538. // 总上传记录数
  1539. const char* sql1 = "SELECT COUNT(*) FROM upload_records;";
  1540. sqlite3_stmt* stmt = nullptr;
  1541. if (sqlite3_prepare_v2(g_db, sql1, -1, &stmt, nullptr) == SQLITE_OK) {
  1542. if (sqlite3_step(stmt) == SQLITE_ROW) stats.total_uploads = sqlite3_column_int(stmt, 0);
  1543. sqlite3_finalize(stmt);
  1544. }
  1545. // 今日上传记录数
  1546. const char* sql2 = "SELECT COUNT(*) FROM upload_records WHERE date(create_time) = date('now', 'localtime');";
  1547. if (sqlite3_prepare_v2(g_db, sql2, -1, &stmt, nullptr) == SQLITE_OK) {
  1548. if (sqlite3_step(stmt) == SQLITE_ROW) stats.today_uploads = sqlite3_column_int(stmt, 0);
  1549. sqlite3_finalize(stmt);
  1550. }
  1551. // 有重量匹配的记录数(两表 tb_num + station_type 关联成功)
  1552. // fix24 v43 Bug#16: 必须带 station_type,同一联单进出站共用 tb_num,
  1553. // 缺条件会导致进站行错误匹配到出站重量(同联单最新记录)
  1554. const char* sql3 = R"(
  1555. SELECT COUNT(DISTINCT ur.id)
  1556. FROM upload_records ur
  1557. INNER JOIN weight_records wr ON ur.tb_num = wr.tb_num
  1558. AND ur.plate_number = wr.plate_number
  1559. AND ur.station_type = wr.station_type;
  1560. )";
  1561. if (sqlite3_prepare_v2(g_db, sql3, -1, &stmt, nullptr) == SQLITE_OK) {
  1562. if (sqlite3_step(stmt) == SQLITE_ROW) stats.matched_weight = sqlite3_column_int(stmt, 0);
  1563. sqlite3_finalize(stmt);
  1564. }
  1565. // 今日匹配数(Bug#16: 同样补 station_type)
  1566. const char* sql4 = R"(
  1567. SELECT COUNT(DISTINCT ur.id)
  1568. FROM upload_records ur
  1569. INNER JOIN weight_records wr ON ur.tb_num = wr.tb_num
  1570. AND ur.plate_number = wr.plate_number
  1571. AND ur.station_type = wr.station_type
  1572. WHERE date(ur.create_time) = date('now', 'localtime');
  1573. )";
  1574. if (sqlite3_prepare_v2(g_db, sql4, -1, &stmt, nullptr) == SQLITE_OK) {
  1575. if (sqlite3_step(stmt) == SQLITE_ROW) stats.today_matched = sqlite3_column_int(stmt, 0);
  1576. sqlite3_finalize(stmt);
  1577. }
  1578. // 重量上传状态统计
  1579. const char* sql5 = R"(
  1580. SELECT
  1581. SUM(CASE WHEN upload_status = 1 THEN 1 ELSE 0 END) AS ok_count,
  1582. SUM(CASE WHEN upload_status = 2 THEN 1 ELSE 0 END) AS fail_count,
  1583. SUM(CASE WHEN upload_status = 0 THEN 1 ELSE 0 END) AS pending_count
  1584. FROM weight_records;
  1585. )";
  1586. if (sqlite3_prepare_v2(g_db, sql5, -1, &stmt, nullptr) == SQLITE_OK) {
  1587. if (sqlite3_step(stmt) == SQLITE_ROW) {
  1588. stats.weight_upload_ok = sqlite3_column_int(stmt, 0);
  1589. stats.weight_upload_fail = sqlite3_column_int(stmt, 1);
  1590. stats.weight_pending = sqlite3_column_int(stmt, 2);
  1591. }
  1592. sqlite3_finalize(stmt);
  1593. }
  1594. // 计算匹配率
  1595. if (stats.total_uploads > 0) {
  1596. stats.match_rate = (double)stats.matched_weight / stats.total_uploads * 100.0;
  1597. }
  1598. if (stats.today_uploads > 0) {
  1599. stats.today_match_rate = (double)stats.today_matched / stats.today_uploads * 100.0;
  1600. }
  1601. return stats;
  1602. }