/* * 增强版考勤服务器 - 支持AES加密数据 * * 功能: * 1. HTTP端口8082 - 接收STM32加密签到数据 * 2. HTTP API - 提供Qt客户端接口 * 3. AES-128-CBC解密支持 * * 编译:g++ -o server_encrypted.exe server_encrypted.cpp -lws2_32 -lcrypto -lssl -lsqlite3 -std=c++11 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #include #include #pragma comment(lib, "ws2_32.lib") typedef int socklen_t; #else #include #include #include #include #include #include #include #define SOCKET int #define INVALID_SOCKET -1 #define SOCKET_ERROR -1 #define closesocket(s) close(s) #define _mkdir(d) mkdir(d, 0755) #endif // OpenSSL库用于AES解密和Base64 #include #include #include #include #include const int HTTP_PORT = 8082; const int BUFFER_SIZE = 4096; const std::string SQLITE_DB_FILE = "attendance.db"; const std::string LOG_FILE = "attendance.log"; const std::string DB_FILE = "attendance_database.csv"; const std::string TIMESLOTS_FILE = "timeslots_config.json"; // 时间段配置文件 const std::string MAPPING_CONFIG_FILE = "mapping_config.json"; // 映射配置文件 const std::string FACE_ID_MAPPING_FILE = "face_id_mapping.json"; // 人脸ID映射文件 const std::string FACES_DIR = "faces"; // 人脸照片目录 const std::string PENDING_DELETE_FILE = "pending_delete.json"; // 待删除人脸列表 const std::string SYNC_REQUEST_FILE = "sync_request.json"; // K210同步请求标志文件 const std::string SYNC_VERSION_FILE = "sync_version.txt"; // 全局同步版本号 const std::string DB_BACKUP_DIR = "backups"; const size_t MAX_JSON_BODY_SIZE = 64 * 1024; const size_t MAX_PHOTO_BODY_SIZE = 512 * 1024; const char* DEFAULT_API_TOKEN = "atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV"; const char* DEFAULT_CORS_ORIGIN = "http://localhost"; const char* DEFAULT_TIMESLOTS_JSON = "{\"time_slots\":[" "{\"name\":\"上午签到\",\"start_time\":\"08:00\",\"end_time\":\"09:30\",\"enabled\":true}," "{\"name\":\"中午签退\",\"start_time\":\"12:00\",\"end_time\":\"13:00\",\"enabled\":true}," "{\"name\":\"下午签到\",\"start_time\":\"14:00\",\"end_time\":\"15:00\",\"enabled\":true}," "{\"name\":\"晚上签退\",\"start_time\":\"17:00\",\"end_time\":\"18:30\",\"enabled\":true}" "]}"; std::string getEnvOrDefault(const char* name, const char* fallback) { const char* value = std::getenv(name); if (value && value[0] != '\0') { return std::string(value); } return std::string(fallback); } const std::string& apiToken() { static const std::string token = getEnvOrDefault("ATTENDANCE_API_TOKEN", DEFAULT_API_TOKEN); return token; } const std::string& allowedCorsOrigin() { static const std::string origin = getEnvOrDefault("ATTENDANCE_CORS_ORIGIN", DEFAULT_CORS_ORIGIN); return origin; } const std::string& bindAddress() { static const std::string address = getEnvOrDefault("ATTENDANCE_BIND_HOST", "0.0.0.0"); return address; } std::string readFaceIdMapping(); std::string getPendingDeleteList(); std::string getFacePhotosList(); std::string getAllStudentIds(); bool setSyncRequest(bool sync); bool removeFromPendingDelete(const std::string& face_id); bool updateDeviceSyncState(const std::string& client, unsigned long long seen_version, unsigned long long ack_version); std::string getDeviceSyncSummary(); bool initializeSqliteStorage(); bool saveAttendanceRecord(const std::string& timestamp, const std::string& student_id, const std::string& name, const std::string& confidence, const std::string& status, const std::string& class_id); std::string readMappingConfig(); bool saveMappingConfig(const std::string& json_data); std::map parseFaceIdMappings(const std::string& json_data); std::string jsonEscape(const std::string& value) { std::ostringstream out; for (unsigned char c : value) { switch (c) { case '\"': out << "\\\""; break; case '\\': out << "\\\\"; break; case '\b': out << "\\b"; break; case '\f': out << "\\f"; break; case '\n': out << "\\n"; break; case '\r': out << "\\r"; break; case '\t': out << "\\t"; break; default: if (c < 0x20) { const char* hex = "0123456789abcdef"; out << "\\u00" << hex[(c >> 4) & 0x0f] << hex[c & 0x0f]; } else { out << static_cast(c); } break; } } return out.str(); } std::string csvEscape(const std::string& value) { bool needs_quotes = false; for (char c : value) { if (c == ',' || c == '"' || c == '\r' || c == '\n') { needs_quotes = true; break; } } if (!needs_quotes) { return value; } std::string escaped = "\""; for (char c : value) { if (c == '"') { escaped += "\"\""; } else { escaped += c; } } escaped += "\""; return escaped; } std::vector parseCsvLine(const std::string& line) { std::vector fields; std::string current; bool in_quotes = false; for (size_t i = 0; i < line.size(); ++i) { char c = line[i]; if (in_quotes) { if (c == '"' && i + 1 < line.size() && line[i + 1] == '"') { current += '"'; ++i; } else if (c == '"') { in_quotes = false; } else { current += c; } } else if (c == '"') { in_quotes = true; } else if (c == ',') { fields.push_back(current); current.clear(); } else { current += c; } } fields.push_back(current); return fields; } bool parseIntInRange(const std::string& value, int min_value, int max_value, int& output) { if (value.empty()) { return false; } try { size_t pos = 0; int parsed = std::stoi(value, &pos); if (pos != value.size() || parsed < min_value || parsed > max_value) { return false; } output = parsed; return true; } catch (...) { return false; } } bool parseTimeParts(const std::string& value, int& hour, int& minute) { if (value.size() != 5 || value[2] != ':' || !std::isdigit(static_cast(value[0])) || !std::isdigit(static_cast(value[1])) || !std::isdigit(static_cast(value[3])) || !std::isdigit(static_cast(value[4]))) { return false; } int parsed_hour = (value[0] - '0') * 10 + (value[1] - '0'); int parsed_minute = (value[3] - '0') * 10 + (value[4] - '0'); if (parsed_hour < 0 || parsed_hour > 23 || parsed_minute < 0 || parsed_minute > 59) { return false; } hour = parsed_hour; minute = parsed_minute; return true; } bool isSafeId(const std::string& value) { if (value.empty() || value.size() > 64) { return false; } for (unsigned char c : value) { if (!(std::isalnum(c) || c == '_' || c == '-')) { return false; } } return true; } bool buildFacePhotoPath(const std::string& face_id, std::string& filepath) { if (!isSafeId(face_id)) { return false; } filepath = FACES_DIR + "/" + face_id + ".jpg"; return true; } bool getHeaderValue(const std::string& request, const std::string& header_name, std::string& output) { size_t headers_end = request.find("\r\n\r\n"); std::string headers = headers_end == std::string::npos ? request : request.substr(0, headers_end); std::istringstream stream(headers); std::string line; std::string expected_key = header_name; std::transform(expected_key.begin(), expected_key.end(), expected_key.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); while (std::getline(stream, line)) { if (!line.empty() && line.back() == '\r') { line.pop_back(); } size_t colon = line.find(':'); if (colon == std::string::npos) { continue; } std::string key = line.substr(0, colon); std::string value = line.substr(colon + 1); key.erase(std::remove_if(key.begin(), key.end(), [](unsigned char c) { return std::isspace(c); }), key.end()); std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (key != expected_key) { continue; } size_t first = value.find_first_not_of(" \t"); if (first == std::string::npos) { output.clear(); } else { value.erase(0, first); size_t last = value.find_last_not_of(" \t"); if (last != std::string::npos) { value.erase(last + 1); } output = value; } return true; } return false; } bool hasHeaderToken(const std::string& request, const std::string& header_name, const std::string& expected) { size_t headers_end = request.find("\r\n\r\n"); std::string headers = headers_end == std::string::npos ? request : request.substr(0, headers_end); std::istringstream stream(headers); std::string line; while (std::getline(stream, line)) { if (!line.empty() && line.back() == '\r') { line.pop_back(); } size_t colon = line.find(':'); if (colon == std::string::npos) { continue; } std::string key = line.substr(0, colon); std::string value = line.substr(colon + 1); key.erase(std::remove_if(key.begin(), key.end(), [](unsigned char c) { return std::isspace(c); }), key.end()); size_t first = value.find_first_not_of(" \t"); if (first == std::string::npos) { value.clear(); } else { value.erase(0, first); size_t last = value.find_last_not_of(" \t"); if (last != std::string::npos) { value.erase(last + 1); } } std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); std::string expected_key = header_name; std::transform(expected_key.begin(), expected_key.end(), expected_key.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (key == expected_key && value == expected) { return true; } } return false; } bool isPublicEndpoint(const std::string& method, const std::string& endpoint) { return method == "OPTIONS" || (method == "GET" && endpoint == "/api/health"); } std::string normalizeApiEndpoint(const std::string& method, const std::string& endpoint) { if (endpoint.find("/api/v1/") != 0) { return endpoint; } if (endpoint == "/api/v1/system/health") return "/api/health"; if (endpoint == "/api/v1/system/time") return "/api/time"; if (method == "POST" && endpoint == "/api/v1/attendance/checkins") return "/api/checkin"; if (method == "GET" && endpoint == "/api/v1/attendance/records") return "/api/records"; if (method == "DELETE" && endpoint == "/api/v1/attendance/records") return "/api/record"; if (method == "POST" && endpoint == "/api/v1/attendance/records/clear") return "/api/clear"; if (method == "GET" && endpoint == "/api/v1/attendance/statistics") return "/api/statistics"; if (method == "GET" && endpoint == "/api/v1/attendance/duplicates") return "/api/check_duplicate"; if (method == "GET" && endpoint == "/api/v1/attendance/today") return "/api/today_records"; if (endpoint == "/api/v1/config/time-slots") return "/api/timeslots"; if (endpoint == "/api/v1/config/mappings") return "/api/mapping_config"; if (endpoint == "/api/v1/faces/mappings") return "/api/face_id_mapping"; if (endpoint == "/api/v1/faces/photos") return "/api/face_photos"; if (endpoint.find("/api/v1/faces/photos/") == 0) { const std::string id = endpoint.substr(std::string("/api/v1/faces/photos/").size()); return method == "POST" ? "/api/face_photo_simple/" + id : "/api/face_photo/" + id; } if (endpoint == "/api/v1/faces/student-ids") return "/api/all_student_ids"; if (endpoint == "/api/v1/faces/pending-delete") return "/api/pending_delete"; if (method == "POST" && endpoint == "/api/v1/faces/delete-ack") return "/api/confirm_delete"; if (method == "DELETE" && endpoint.find("/api/v1/faces/") == 0) { return "/api/face/" + endpoint.substr(std::string("/api/v1/faces/").size()); } if (endpoint == "/api/v1/sync/request") return "/api/sync_request"; if (endpoint == "/api/v1/sync/request/ack") return "/api/sync_ack"; if (endpoint == "/api/v1/sync/trigger") return "/api/trigger_sync"; return endpoint; } bool isAuthorizedRequest(const std::string& request, const std::string& method, const std::string& endpoint) { if (isPublicEndpoint(method, endpoint)) { return true; } return hasHeaderToken(request, "X-Api-Token", apiToken()); } std::string makeJsonError(const std::string& message) { return "{\"success\":false,\"message\":\"" + jsonEscape(message) + "\"}"; } // ⚠️ 重要:密钥和IV必须与STM32端完全一致! static const unsigned char MY_AES_KEY[16] = { 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c }; static const unsigned char MY_AES_IV[16] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }; // ============= Base64解码 ============= std::string base64_decode(const std::string& encoded_string) { BIO *bio, *b64; int decode_len = encoded_string.length(); char* buffer = new char[decode_len]; bio = BIO_new_mem_buf(encoded_string.c_str(), -1); b64 = BIO_new(BIO_f_base64()); bio = BIO_push(b64, bio); BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); int length = BIO_read(bio, buffer, decode_len); BIO_free_all(bio); std::string result(buffer, length); delete[] buffer; return result; } // ============= 添加PKCS#7填充 ============= std::string add_pkcs7_padding(const std::string& data) { size_t padding_len = 16 - (data.length() % 16); std::string result = data; for (size_t i = 0; i < padding_len; i++) { result += (char)padding_len; } return result; } // ============= 手动移除PKCS#7填充 ============= std::string remove_pkcs7_padding(const std::string& data) { if (data.empty()) return ""; // 获取最后一个字节(填充长度) unsigned char padding_len = (unsigned char)data[data.length() - 1]; // 验证填充长度 if (padding_len < 1 || padding_len > 16 || padding_len > data.length()) { std::cerr << "[CRYPTO] Invalid padding length: " << (int)padding_len << std::endl; return data; // 返回原数据 } // 验证所有填充字节都相同 for (size_t i = data.length() - padding_len; i < data.length(); i++) { if ((unsigned char)data[i] != padding_len) { std::cerr << "[CRYPTO] Invalid padding at position " << i << std::endl; return data; // 返回原数据 } } // 移除填充 return data.substr(0, data.length() - padding_len); } // ============= AES-CBC加密 (用于响应Qt客户端) ============= std::string aes_encrypt_cbc(const std::string& plaintext) { // 添加PKCS#7填充 std::string padded = add_pkcs7_padding(plaintext); EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); if (!ctx) { std::cerr << "[CRYPTO] Failed to create context" << std::endl; return ""; } // 初始化加密 if (EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, MY_AES_KEY, MY_AES_IV) != 1) { std::cerr << "[CRYPTO] EncryptInit failed" << std::endl; EVP_CIPHER_CTX_free(ctx); return ""; } // 禁用自动填充(我们已手动添加) EVP_CIPHER_CTX_set_padding(ctx, 0); // 加密数据 std::vector ciphertext(padded.length() + 16); int len; int ciphertext_len = 0; if (EVP_EncryptUpdate(ctx, ciphertext.data(), &len, (unsigned char*)padded.c_str(), padded.length()) != 1) { std::cerr << "[CRYPTO] EncryptUpdate failed" << std::endl; EVP_CIPHER_CTX_free(ctx); return ""; } ciphertext_len = len; // 完成加密 if (EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len) != 1) { std::cerr << "[CRYPTO] EncryptFinal failed" << std::endl; EVP_CIPHER_CTX_free(ctx); return ""; } ciphertext_len += len; EVP_CIPHER_CTX_free(ctx); return std::string((char*)ciphertext.data(), ciphertext_len); } // ============= AES-CBC解密 (使用OpenSSL标准库) ============= std::string aes_decrypt_cbc(const std::string& encrypted_data) { if (encrypted_data.empty() || encrypted_data.length() % 16 != 0) { std::cerr << "[CRYPTO] Invalid encrypted data length: " << encrypted_data.length() << std::endl; return ""; } EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); if (!ctx) { std::cerr << "[CRYPTO] Failed to create context" << std::endl; return ""; } // 初始化解密(使用标准AES-128-CBC) if (EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, MY_AES_KEY, MY_AES_IV) != 1) { std::cerr << "[CRYPTO] DecryptInit failed" << std::endl; EVP_CIPHER_CTX_free(ctx); return ""; } // 禁用自动填充,我们手动处理PKCS#7 EVP_CIPHER_CTX_set_padding(ctx, 0); // 解密数据 std::vector plaintext(encrypted_data.length() + 16); int len; int plaintext_len = 0; if (EVP_DecryptUpdate(ctx, plaintext.data(), &len, (unsigned char*)encrypted_data.c_str(), encrypted_data.length()) != 1) { std::cerr << "[CRYPTO] DecryptUpdate failed" << std::endl; EVP_CIPHER_CTX_free(ctx); return ""; } plaintext_len = len; // 完成解密 if (EVP_DecryptFinal_ex(ctx, plaintext.data() + len, &len) != 1) { std::cerr << "[CRYPTO] DecryptFinal failed" << std::endl; EVP_CIPHER_CTX_free(ctx); return ""; } plaintext_len += len; EVP_CIPHER_CTX_free(ctx); std::string result((char*)plaintext.data(), plaintext_len); // 手动移除PKCS#7填充 result = remove_pkcs7_padding(result); return result; } // ============= 十六进制输出辅助函数 ============= void print_hex(const std::string& label, const unsigned char* data, size_t len) { std::cout << label << " (" << len << " bytes): "; for (size_t i = 0; i < len && i < 32; i++) { printf("%02x ", data[i]); } if (len > 32) std::cout << "..."; std::cout << std::endl; } // ============= Base64编码 ============= std::string base64_encode(const std::string& input) { BIO *bio, *b64; BUF_MEM *bufferPtr; b64 = BIO_new(BIO_f_base64()); bio = BIO_new(BIO_s_mem()); bio = BIO_push(b64, bio); BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); BIO_write(bio, input.c_str(), input.length()); BIO_flush(bio); BIO_get_mem_ptr(bio, &bufferPtr); std::string result(bufferPtr->data, bufferPtr->length); BIO_free_all(bio); return result; } // ============= 加密JSON数据(用于响应Qt客户端)============= std::string encryptJSON(const std::string& json_data) { std::cout << "[CRYPTO] Encrypting response..." << std::endl; // AES加密 std::string encrypted = aes_encrypt_cbc(json_data); if (encrypted.empty()) { std::cerr << "[CRYPTO] Encryption failed!" << std::endl; return ""; } // Base64编码 std::string encrypted_base64 = base64_encode(encrypted); std::cout << "[CRYPTO] ✓ Response encrypted" << std::endl; return encrypted_base64; } // ============= 解密JSON数据 ============= std::string decryptJSON(const std::string& encrypted_base64) { try { std::cout << "[CRYPTO] Decrypting data..." << std::endl; std::cout << "[CRYPTO] Base64 input: " << encrypted_base64 << std::endl; std::cout << "[CRYPTO] Base64 length: " << encrypted_base64.length() << std::endl; // 1. Base64解码 std::string encrypted_binary = base64_decode(encrypted_base64); std::cout << "[CRYPTO] Binary length: " << encrypted_binary.length() << std::endl; print_hex("[CRYPTO] Encrypted binary", (unsigned char*)encrypted_binary.c_str(), encrypted_binary.length()); // 调试:打印密钥和IV print_hex("[CRYPTO] Using KEY", MY_AES_KEY, 16); print_hex("[CRYPTO] Using IV", MY_AES_IV, 16); // 2. AES解密 std::string decrypted = aes_decrypt_cbc(encrypted_binary); if (decrypted.empty()) { std::cerr << "[CRYPTO] Decryption failed!" << std::endl; return ""; } std::cout << "[CRYPTO] ✓ Decryption successful" << std::endl; std::cout << "[CRYPTO] Decrypted length: " << decrypted.length() << std::endl; print_hex("[CRYPTO] Decrypted hex", (unsigned char*)decrypted.c_str(), decrypted.length()); std::cout << "[CRYPTO] Decrypted text: " << decrypted << std::endl; return decrypted; } catch (const std::exception& e) { std::cerr << "[CRYPTO] Exception: " << e.what() << std::endl; return ""; } } // ============= 工具函数 ============= std::string getCurrentTimestamp() { time_t now = time(0); tm ltm; #ifdef _WIN32 localtime_s(<m, &now); #else localtime_r(&now, <m); #endif char buffer[80]; strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", <m); return std::string(buffer); } // 清理字符串中的控制字符(\r, \n等) std::string cleanString(const std::string& str) { std::string result; result.reserve(str.length()); for (char c : str) { // 只保留可打印字符和空格,过滤掉\r\n\t等控制字符 if (c >= 32 && c <= 126) { result += c; } } return result; } std::string urlDecode(const std::string& str) { std::string result; for (size_t i = 0; i < str.length(); ++i) { if (str[i] == '%' && i + 2 < str.length()) { int value; std::istringstream is(str.substr(i + 1, 2)); if (is >> std::hex >> value) { result += static_cast(value); i += 2; } else { result += str[i]; } } else if (str[i] == '+') { result += ' '; } else { result += str[i]; } } return result; } std::map parseQueryParams(const std::string& query) { std::map params; std::istringstream stream(query); std::string pair; while (std::getline(stream, pair, '&')) { size_t pos = pair.find('='); if (pos != std::string::npos) { std::string key = urlDecode(pair.substr(0, pos)); std::string value = urlDecode(pair.substr(pos + 1)); params[key] = value; } } return params; } bool parseJsonStringLiteral(const std::string& json, size_t& pos, std::string& output) { if (pos >= json.size() || json[pos] != '"') return false; pos++; output.clear(); while (pos < json.size()) { char c = json[pos++]; if (c == '"') { return true; } if (c != '\\') { output += c; continue; } if (pos >= json.size()) return false; char esc = json[pos++]; switch (esc) { case '"': output += '"'; break; case '\\': output += '\\'; break; case '/': output += '/'; break; case 'b': output += '\b'; break; case 'f': output += '\f'; break; case 'n': output += '\n'; break; case 'r': output += '\r'; break; case 't': output += '\t'; break; case 'u': // 当前接口字段只需要保留UTF-8原文;遇到unicode转义时不执行错误拆分。 output += "\\u"; for (int i = 0; i < 4 && pos < json.size(); ++i) { output += json[pos++]; } break; default: output += esc; break; } } return false; } // 解析签到接口使用的扁平JSON对象,支持字符串转义、逗号和冒号。 std::map parseJSON(const std::string& json) { std::map result; size_t pos = json.find('{'); if (pos == std::string::npos) return result; pos++; while (pos < json.size()) { while (pos < json.size() && std::isspace(static_cast(json[pos]))) pos++; if (pos >= json.size() || json[pos] == '}') break; std::string key; if (!parseJsonStringLiteral(json, pos, key)) { result.clear(); return result; } while (pos < json.size() && std::isspace(static_cast(json[pos]))) pos++; if (pos >= json.size() || json[pos] != ':') { result.clear(); return result; } pos++; while (pos < json.size() && std::isspace(static_cast(json[pos]))) pos++; std::string value; if (pos < json.size() && json[pos] == '"') { if (!parseJsonStringLiteral(json, pos, value)) { result.clear(); return result; } } else { size_t start = pos; while (pos < json.size() && json[pos] != ',' && json[pos] != '}') pos++; value = json.substr(start, pos - start); size_t first = value.find_first_not_of(" \t\r\n"); size_t last = value.find_last_not_of(" \t\r\n"); value = first == std::string::npos ? "" : value.substr(first, last - first + 1); } result[key] = value; while (pos < json.size() && std::isspace(static_cast(json[pos]))) pos++; if (pos < json.size() && json[pos] == ',') pos++; } return result; } // ============= SQLite存储层 ============= sqlite3* openSqliteDb() { sqlite3* db = nullptr; if (sqlite3_open(SQLITE_DB_FILE.c_str(), &db) != SQLITE_OK) { std::cerr << "[SQLITE] Open failed: " << (db ? sqlite3_errmsg(db) : "unknown") << std::endl; if (db) sqlite3_close(db); return nullptr; } sqlite3_busy_timeout(db, 3000); return db; } bool sqliteExec(sqlite3* db, const std::string& sql) { char* err = nullptr; if (sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &err) != SQLITE_OK) { std::cerr << "[SQLITE] Exec failed: " << (err ? err : sqlite3_errmsg(db)) << std::endl; sqlite3_free(err); return false; } return true; } std::string sqliteColumnText(sqlite3_stmt* stmt, int col) { const unsigned char* text = sqlite3_column_text(stmt, col); return text ? reinterpret_cast(text) : ""; } bool sqliteBindText(sqlite3_stmt* stmt, int index, const std::string& value) { return sqlite3_bind_text(stmt, index, value.c_str(), -1, SQLITE_TRANSIENT) == SQLITE_OK; } bool sqliteTableHasRows(sqlite3* db, const std::string& table) { std::string sql = "SELECT 1 FROM " + table + " LIMIT 1"; sqlite3_stmt* stmt = nullptr; bool has_rows = false; if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) { has_rows = sqlite3_step(stmt) == SQLITE_ROW; } sqlite3_finalize(stmt); return has_rows; } bool sqliteTableHasColumn(sqlite3* db, const std::string& table, const std::string& column) { sqlite3_stmt* stmt = nullptr; bool found = false; std::string sql = "PRAGMA table_info(" + table + ")"; if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { if (sqliteColumnText(stmt, 1) == column) { found = true; break; } } } sqlite3_finalize(stmt); return found; } bool copyFileBinary(const std::string& src, const std::string& dst) { std::ifstream in(src.c_str(), std::ios::binary); if (!in.is_open()) return false; std::ofstream out(dst.c_str(), std::ios::binary); if (!out.is_open()) return false; out << in.rdbuf(); return out.good(); } bool backupSqliteDatabaseOnStartup() { std::ifstream check(SQLITE_DB_FILE.c_str(), std::ios::binary); if (!check.is_open()) { return true; } check.close(); _mkdir(DB_BACKUP_DIR.c_str()); std::string ts = getCurrentTimestamp(); std::replace(ts.begin(), ts.end(), ':', '-'); std::replace(ts.begin(), ts.end(), ' ', '_'); const std::string backup_path = DB_BACKUP_DIR + "/attendance_" + ts + ".db"; if (!copyFileBinary(SQLITE_DB_FILE, backup_path)) { std::cerr << "[BACKUP] Failed to create SQLite backup: " << backup_path << std::endl; return false; } std::cout << "[BACKUP] SQLite backup created: " << backup_path << std::endl; return true; } std::string readWholeFileOrDefault(const std::string& path, const std::string& fallback) { std::ifstream file(path.c_str(), std::ios::binary); if (!file.is_open()) return fallback; std::stringstream buffer; buffer << file.rdbuf(); std::string content = buffer.str(); return content.empty() ? fallback : content; } bool sqliteSetKv(sqlite3* db, const std::string& key, const std::string& value) { sqlite3_stmt* stmt = nullptr; const char* sql = "REPLACE INTO kv_store(key,value) VALUES(?,?)"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false; sqliteBindText(stmt, 1, key); sqliteBindText(stmt, 2, value); bool ok = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return ok; } std::string sqliteGetKv(sqlite3* db, const std::string& key, const std::string& fallback) { sqlite3_stmt* stmt = nullptr; std::string value = fallback; const char* sql = "SELECT value FROM kv_store WHERE key=?"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, key); if (sqlite3_step(stmt) == SQLITE_ROW) { value = sqliteColumnText(stmt, 0); } } sqlite3_finalize(stmt); return value; } bool sqliteSetKvValue(const std::string& key, const std::string& value) { sqlite3* db = openSqliteDb(); if (!db) return false; bool ok = sqliteSetKv(db, key, value); sqlite3_close(db); return ok; } std::string sqliteGetKvValue(const std::string& key, const std::string& fallback) { sqlite3* db = openSqliteDb(); if (!db) return fallback; std::string value = sqliteGetKv(db, key, fallback); sqlite3_close(db); return value; } int sqliteCountRows(sqlite3* db, const std::string& table) { sqlite3_stmt* stmt = nullptr; std::string sql = "SELECT COUNT(*) FROM " + table; int count = 0; if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK && sqlite3_step(stmt) == SQLITE_ROW) { count = sqlite3_column_int(stmt, 0); } sqlite3_finalize(stmt); return count; } bool sqliteQuickCheck(std::string& message) { sqlite3* db = openSqliteDb(); if (!db) { message = "open_failed"; return false; } sqlite3_stmt* stmt = nullptr; bool ok = false; message = "unknown"; if (sqlite3_prepare_v2(db, "PRAGMA quick_check", -1, &stmt, nullptr) == SQLITE_OK && sqlite3_step(stmt) == SQLITE_ROW) { message = sqliteColumnText(stmt, 0); ok = message == "ok"; } sqlite3_finalize(stmt); sqlite3_close(db); return ok; } int countFacePhotoFiles() { int count = 0; #ifdef _WIN32 WIN32_FIND_DATAA findData; std::string searchPath = FACES_DIR + "\\*.jpg"; HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData); if (hFind != INVALID_HANDLE_VALUE) { do { count++; } while (FindNextFileA(hFind, &findData)); FindClose(hFind); } #else DIR* dir = opendir(FACES_DIR.c_str()); if (dir) { struct dirent* entry; while ((entry = readdir(dir)) != NULL) { std::string name = entry->d_name; if (name.size() > 4 && name.substr(name.size() - 4) == ".jpg") { count++; } } closedir(dir); } #endif return count; } bool sqliteInsertRecord(sqlite3* db, const std::string& timestamp, const std::string& student_id, const std::string& name, const std::string& confidence, const std::string& status, const std::string& class_id) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO records(timestamp,student_id,name,confidence,status,class_id) " "VALUES(?,?,?,?,?,?)"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false; sqliteBindText(stmt, 1, timestamp); sqliteBindText(stmt, 2, student_id); sqliteBindText(stmt, 3, name); sqliteBindText(stmt, 4, confidence); sqliteBindText(stmt, 5, status); sqliteBindText(stmt, 6, class_id.empty() ? "0" : class_id); bool ok = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return ok; } bool saveAttendanceRecord(const std::string& timestamp, const std::string& student_id, const std::string& name, const std::string& confidence, const std::string& status, const std::string& class_id) { sqlite3* db = openSqliteDb(); if (!db) return false; bool ok = sqliteInsertRecord(db, timestamp, student_id, name, confidence, status, class_id); sqlite3_close(db); return ok; } std::vector parseJsonStringList(const std::string& json, const std::string& key) { std::vector values; std::string marker = "\"" + key + "\":["; size_t start = json.find(marker); if (start == std::string::npos) return values; start = json.find('[', start); size_t end = json.find(']', start); if (start == std::string::npos || end == std::string::npos) return values; std::string arr = json.substr(start + 1, end - start - 1); std::istringstream ss(arr); std::string token; while (std::getline(ss, token, ',')) { token.erase(std::remove(token.begin(), token.end(), '"'), token.end()); token.erase(std::remove_if(token.begin(), token.end(), [](unsigned char c) { return std::isspace(c); }), token.end()); if (!token.empty()) values.push_back(token); } return values; } bool initializeSqliteStorage() { sqlite3* db = openSqliteDb(); if (!db) return false; bool ok = sqliteExec(db, "PRAGMA journal_mode=WAL;" "CREATE TABLE IF NOT EXISTS records(" "id INTEGER PRIMARY KEY AUTOINCREMENT," "timestamp TEXT NOT NULL," "student_id TEXT NOT NULL," "name TEXT," "confidence TEXT," "status TEXT," "class_id TEXT DEFAULT '0'" ");" "CREATE INDEX IF NOT EXISTS idx_records_student_time ON records(student_id,timestamp);" "CREATE INDEX IF NOT EXISTS idx_records_timestamp ON records(timestamp);" "CREATE TABLE IF NOT EXISTS kv_store(" "key TEXT PRIMARY KEY," "value TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS face_mappings(" "face_id TEXT PRIMARY KEY," "student_id TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS pending_delete(" "task_id TEXT PRIMARY KEY," "face_id TEXT NOT NULL," "student_id TEXT," "created_at TEXT NOT NULL," "status TEXT NOT NULL DEFAULT 'pending'," "ack_at TEXT" ");" "CREATE INDEX IF NOT EXISTS idx_pending_delete_face ON pending_delete(face_id);" "CREATE TABLE IF NOT EXISTS device_sync_state(" "client TEXT PRIMARY KEY," "last_seen_version INTEGER NOT NULL DEFAULT 0," "last_ack_version INTEGER NOT NULL DEFAULT 0," "last_seen_at TEXT NOT NULL" ");"); if (!ok) { sqlite3_close(db); return false; } if (!sqliteTableHasColumn(db, "pending_delete", "task_id")) { sqliteExec(db, "ALTER TABLE pending_delete ADD COLUMN task_id TEXT"); } if (!sqliteTableHasColumn(db, "pending_delete", "student_id")) { sqliteExec(db, "ALTER TABLE pending_delete ADD COLUMN student_id TEXT"); } if (!sqliteTableHasColumn(db, "pending_delete", "created_at")) { sqliteExec(db, "ALTER TABLE pending_delete ADD COLUMN created_at TEXT"); } if (!sqliteTableHasColumn(db, "pending_delete", "status")) { sqliteExec(db, "ALTER TABLE pending_delete ADD COLUMN status TEXT DEFAULT 'pending'"); } if (!sqliteTableHasColumn(db, "pending_delete", "ack_at")) { sqliteExec(db, "ALTER TABLE pending_delete ADD COLUMN ack_at TEXT"); } sqliteExec(db, "UPDATE pending_delete SET task_id='legacy-' || face_id WHERE task_id IS NULL OR task_id=''"); sqliteExec(db, "UPDATE pending_delete SET created_at='" + getCurrentTimestamp() + "' WHERE created_at IS NULL OR created_at=''"); sqliteExec(db, "UPDATE pending_delete SET status='pending' WHERE status IS NULL OR status=''"); if (!sqliteTableHasRows(db, "records")) { std::ifstream csv(DB_FILE.c_str()); if (csv.is_open()) { std::string line; std::getline(csv, line); int imported = 0; sqliteExec(db, "BEGIN"); while (std::getline(csv, line)) { if (line.empty()) continue; std::vector fields = parseCsvLine(line); std::string timestamp = fields.size() > 0 ? fields[0] : ""; std::string student_id = fields.size() > 1 ? fields[1] : ""; std::string name = fields.size() > 2 ? fields[2] : ""; std::string confidence = fields.size() > 3 ? fields[3] : "0"; std::string status = fields.size() > 4 ? fields[4] : ""; std::string class_id = fields.size() > 5 ? fields[5] : "0"; if (!timestamp.empty() && !student_id.empty() && sqliteInsertRecord(db, timestamp, student_id, name, confidence, status, class_id)) { imported++; } } sqliteExec(db, "COMMIT"); std::cout << "[SQLITE] Imported records from CSV: " << imported << std::endl; } } if (sqliteGetKv(db, "time_slots", "").empty()) { sqliteSetKv(db, "time_slots", readWholeFileOrDefault(TIMESLOTS_FILE, DEFAULT_TIMESLOTS_JSON)); } if (sqliteGetKv(db, "mapping_config", "").empty()) { sqliteSetKv(db, "mapping_config", readWholeFileOrDefault(MAPPING_CONFIG_FILE, "{\"student_name_map\":{},\"class_name_map\":{}}")); } if (sqliteGetKv(db, "sync_request", "").empty()) { sqliteSetKv(db, "sync_request", readWholeFileOrDefault(SYNC_REQUEST_FILE, "{\"sync\":false}")); } if (sqliteGetKv(db, "sync_version", "").empty()) { sqliteSetKv(db, "sync_version", readWholeFileOrDefault(SYNC_VERSION_FILE, "1")); } if (!sqliteTableHasRows(db, "face_mappings")) { std::map mappings = parseFaceIdMappings( readWholeFileOrDefault(FACE_ID_MAPPING_FILE, "{\"mappings\":{}}")); sqlite3_stmt* stmt = nullptr; const char* sql = "REPLACE INTO face_mappings(face_id,student_id) VALUES(?,?)"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) { for (const auto& m : mappings) { sqlite3_reset(stmt); sqlite3_clear_bindings(stmt); sqliteBindText(stmt, 1, m.first); sqliteBindText(stmt, 2, m.second); sqlite3_step(stmt); } } sqlite3_finalize(stmt); std::cout << "[SQLITE] Imported face mappings: " << mappings.size() << std::endl; } if (!sqliteTableHasRows(db, "pending_delete")) { std::vector pending = parseJsonStringList( readWholeFileOrDefault(PENDING_DELETE_FILE, "{\"pending_delete\":[]}"), "pending_delete"); sqlite3_stmt* stmt = nullptr; const char* sql = "REPLACE INTO pending_delete(task_id,face_id,student_id,created_at,status) VALUES(?,?,?,?,?)"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) { for (const auto& id : pending) { sqlite3_reset(stmt); sqlite3_clear_bindings(stmt); sqliteBindText(stmt, 1, "legacy-" + id); sqliteBindText(stmt, 2, id); sqliteBindText(stmt, 3, id); sqliteBindText(stmt, 4, getCurrentTimestamp()); sqliteBindText(stmt, 5, "pending"); sqlite3_step(stmt); } } sqlite3_finalize(stmt); std::cout << "[SQLITE] Imported pending deletes: " << pending.size() << std::endl; } _mkdir(FACES_DIR.c_str()); sqlite3_close(db); std::cout << "[SQLITE] Storage ready: " << SQLITE_DB_FILE << std::endl; return true; } // ============= 数据库操作 ============= std::string readDatabase(int limit = -1, int offset = 0) { sqlite3* db = openSqliteDb(); if (!db) return "[]"; std::ostringstream json; json << "["; sqlite3_stmt* stmt = nullptr; std::string sql = "SELECT timestamp,student_id,name,confidence,status,class_id FROM records ORDER BY id DESC"; if (limit > 0) { sql += " LIMIT ? OFFSET ?"; } bool first = true; if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) { if (limit > 0) { sqlite3_bind_int(stmt, 1, limit); sqlite3_bind_int(stmt, 2, offset); } while (sqlite3_step(stmt) == SQLITE_ROW) { if (!first) json << ","; first = false; std::string conf = sqliteColumnText(stmt, 3); if (conf.empty()) conf = "0"; std::string class_id = sqliteColumnText(stmt, 5); if (class_id.empty()) class_id = "0"; json << "{" << "\"timestamp\":\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\"," << "\"student_id\":\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\"," << "\"name\":\"" << jsonEscape(sqliteColumnText(stmt, 2)) << "\"," << "\"confidence\":" << conf << "," << "\"status\":\"" << jsonEscape(sqliteColumnText(stmt, 4)) << "\"," << "\"class_id\":\"" << jsonEscape(class_id) << "\"" << "}"; } } sqlite3_finalize(stmt); sqlite3_close(db); json << "]"; return json.str(); } std::string getStatistics() { sqlite3* db = openSqliteDb(); if (!db) return "{\"total\":0,\"today\":0}"; int total = 0; int today = 0; std::string todayDate = getCurrentTimestamp().substr(0, 10); sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM records", -1, &stmt, nullptr) == SQLITE_OK && sqlite3_step(stmt) == SQLITE_ROW) { total = sqlite3_column_int(stmt, 0); } sqlite3_finalize(stmt); stmt = nullptr; if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM records WHERE substr(timestamp,1,10)=?", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, todayDate); if (sqlite3_step(stmt) == SQLITE_ROW) { today = sqlite3_column_int(stmt, 0); } } sqlite3_finalize(stmt); sqlite3_close(db); std::ostringstream json; json << "{" << "\"total\":" << total << "," << "\"today\":" << today << "," << "\"server_time\":\"" << getCurrentTimestamp() << "\"" << "}"; return json.str(); } bool clearDatabase() { sqlite3* db = openSqliteDb(); if (!db) return false; bool ok = sqliteExec(db, "DELETE FROM records"); sqlite3_close(db); return ok; } // ============= 时间段配置管理 ============= bool saveTimeSlots(const std::string& json_data) { std::cout << "[TIMESLOTS] Attempting to save " << json_data.size() << " bytes" << std::endl; if (json_data.empty()) { std::cerr << "[TIMESLOTS] ERROR: json_data is empty!" << std::endl; return false; } if (!sqliteSetKvValue("time_slots", json_data)) { std::cerr << "[TIMESLOTS] Failed to save configuration to SQLite" << std::endl; return false; } std::cout << "[TIMESLOTS] ✓ Configuration saved successfully to SQLite" << std::endl; std::cout << "[TIMESLOTS] Preview: " << json_data.substr(0, 100) << "..." << std::endl; return true; } std::string readTimeSlots() { std::string content = sqliteGetKvValue("time_slots", DEFAULT_TIMESLOTS_JSON); std::cout << "[TIMESLOTS] Read " << content.size() << " bytes from SQLite" << std::endl; return content; } bool saveMappingConfig(const std::string& json_data) { if (json_data.empty()) return false; return sqliteSetKvValue("mapping_config", json_data); } std::string readMappingConfig() { return sqliteGetKvValue("mapping_config", "{\"student_name_map\":{},\"class_name_map\":{}}"); } unsigned long long readSyncVersion() { unsigned long long version = 1; std::string value = sqliteGetKvValue("sync_version", "1"); std::istringstream ss(value); if (!(ss >> version)) { version = 1; } if (version == 0) version = 1; return version; } unsigned long long bumpSyncVersion(const std::string& reason) { unsigned long long version = readSyncVersion() + 1; sqliteSetKvValue("sync_version", std::to_string(version)); std::cout << "[SYNC] Version bumped to " << version << " (" << reason << ")" << std::endl; return version; } unsigned long long parseVersionParam(const std::map& params) { auto it = params.find("last_version"); if (it == params.end()) return 0; unsigned long long value = 0; for (char c : it->second) { if (!std::isdigit(static_cast(c))) return 0; value = value * 10 + static_cast(c - '0'); } return value; } size_t findMatchingJsonEnd(const std::string& text, size_t start) { if (start >= text.size()) return std::string::npos; const char open = text[start]; const char close = (open == '{') ? '}' : ']'; int depth = 0; bool in_string = false; bool escape = false; for (size_t i = start; i < text.size(); ++i) { char c = text[i]; if (escape) { escape = false; continue; } if (c == '\\' && in_string) { escape = true; continue; } if (c == '"') { in_string = !in_string; continue; } if (in_string) continue; if (c == open) depth++; if (c == close) { depth--; if (depth == 0) return i; } } return std::string::npos; } std::string extractJsonMemberValue(const std::string& json, const std::string& key, const std::string& fallback) { std::string marker = "\"" + key + "\":"; size_t pos = json.find(marker); if (pos == std::string::npos) return fallback; pos += marker.size(); while (pos < json.size() && std::isspace(static_cast(json[pos]))) pos++; if (pos >= json.size()) return fallback; if (json[pos] == '{' || json[pos] == '[') { size_t end = findMatchingJsonEnd(json, pos); if (end != std::string::npos) { return json.substr(pos, end - pos + 1); } } size_t end = json.find_first_of(",}", pos); if (end != std::string::npos) { return json.substr(pos, end - pos); } return fallback; } std::string extractJsonStringValue(const std::string& json, const std::string& key, const std::string& fallback = "") { std::string marker = "\"" + key + "\":\""; size_t pos = json.find(marker); if (pos == std::string::npos) return fallback; pos += marker.size(); size_t end = json.find("\"", pos); if (end == std::string::npos) return fallback; return json.substr(pos, end - pos); } std::vector extractJsonStringArray(const std::string& json, const std::string& key) { std::vector values; std::string arr = extractJsonMemberValue(json, key, "[]"); if (arr.size() < 2) return values; size_t pos = 0; while (pos < arr.size()) { size_t start = arr.find("\"", pos); if (start == std::string::npos) break; size_t end = arr.find("\"", start + 1); if (end == std::string::npos) break; values.push_back(arr.substr(start + 1, end - start - 1)); pos = end + 1; } return values; } unsigned long long jsonNumberValue(const std::string& json, const std::string& key, unsigned long long fallback) { std::string marker = "\"" + key + "\":"; size_t pos = json.find(marker); if (pos == std::string::npos) return fallback; pos += marker.size(); while (pos < json.size() && std::isspace(static_cast(json[pos]))) pos++; unsigned long long value = 0; bool has_digit = false; while (pos < json.size() && std::isdigit(static_cast(json[pos]))) { has_digit = true; value = value * 10ULL + static_cast(json[pos] - '0'); pos++; } return has_digit ? value : fallback; } std::string makeSyncStateResponse(const std::string& client, unsigned long long last_version) { const unsigned long long version = readSyncVersion(); const bool changed = last_version < version; updateDeviceSyncState(client.empty() ? "all" : client, version, 0); std::ostringstream json; json << "{\"version\":" << version << ",\"changed\":" << (changed ? "true" : "false") << ",\"client\":\"" << jsonEscape(client) << "\"" << ",\"server_time\":\"" << jsonEscape(getCurrentTimestamp()) << "\""; if (changed) { json << ",\"state\":{"; bool first = true; if (client == "k210" || client == "qt" || client == "all" || client.empty()) { const std::string mappings = extractJsonMemberValue(readFaceIdMapping(), "mappings", "{}"); const std::string pending_json = getPendingDeleteList(); const std::string pending = extractJsonMemberValue(pending_json, "pending_delete", "[]"); const std::string delete_tasks = extractJsonMemberValue(pending_json, "delete_tasks", "[]"); json << "\"face_mappings\":" << mappings << ",\"pending_delete\":" << pending << ",\"delete_tasks\":" << delete_tasks; first = false; } if (client == "stm32" || client == "qt" || client == "all" || client.empty()) { if (!first) json << ","; const std::string slots = extractJsonMemberValue(readTimeSlots(), "time_slots", "[]"); json << "\"time_slots\":" << slots; first = false; } if (client == "qt" || client == "all" || client.empty()) { if (!first) json << ","; std::string mapping_config = readMappingConfig(); json << "\"mapping_config\":" << mapping_config << ",\"face_photos\":" << extractJsonMemberValue(getFacePhotosList(), "photos", "[]") << ",\"all_student_ids\":" << extractJsonMemberValue(getAllStudentIds(), "student_ids", "[]") << ",\"records\":" << readDatabase(1000, 0) << ",\"statistics\":" << getStatistics(); } json << "}"; } json << "}"; return json.str(); } std::string makeSyncAckResponse(const std::string& request_body) { const std::string client = extractJsonStringValue(request_body, "client", "unknown"); const unsigned long long ack_version = static_cast(jsonNumberValue(request_body, "version", 0)); const std::vector deleted = extractJsonStringArray(request_body, "deleted"); const std::vector task_ids = extractJsonStringArray(request_body, "task_ids"); int confirmed_delete_count = 0; for (const std::string& task_id : task_ids) { if (!task_id.empty() && removeFromPendingDelete(task_id)) { confirmed_delete_count++; } } for (const std::string& id : deleted) { if (!id.empty() && removeFromPendingDelete(id)) { confirmed_delete_count++; } } updateDeviceSyncState(client, readSyncVersion(), ack_version); if (confirmed_delete_count > 0) { bumpSyncVersion("sync_ack_delete"); } std::ostringstream json; json << "{\"success\":true," << "\"client\":\"" << jsonEscape(client) << "\"," << "\"version\":" << readSyncVersion() << "," << "\"confirmed_delete_count\":" << confirmed_delete_count << "}"; return json.str(); } std::string makeSyncVersionResponse() { std::ostringstream json; json << "{" << "\"version\":" << readSyncVersion() << "," << "\"server_time\":\"" << jsonEscape(getCurrentTimestamp()) << "\"" << "}"; return json.str(); } std::string makeStorageStatusResponse() { std::string quick_check_message; const bool quick_ok = sqliteQuickCheck(quick_check_message); sqlite3* db = openSqliteDb(); int records = 0; int face_mappings = 0; int pending_deletes = 0; int face_photos = countFacePhotoFiles(); if (db) { records = sqliteCountRows(db, "records"); face_mappings = sqliteCountRows(db, "face_mappings"); pending_deletes = sqliteCountRows(db, "pending_delete"); sqlite3_close(db); } std::ostringstream json; json << "{" << "\"storage\":\"sqlite\"," << "\"database\":\"" << jsonEscape(SQLITE_DB_FILE) << "\"," << "\"ok\":" << (quick_ok ? "true" : "false") << "," << "\"quick_check\":\"" << jsonEscape(quick_check_message) << "\"," << "\"records\":" << records << "," << "\"face_mappings\":" << face_mappings << "," << "\"face_photos\":" << face_photos << "," << "\"pending_deletes\":" << pending_deletes << "," << "\"sync_version\":" << readSyncVersion() << "," << "\"device_sync_state\":" << getDeviceSyncSummary() << "," << "\"server_time\":\"" << jsonEscape(getCurrentTimestamp()) << "\"" << "}"; return json.str(); } std::string makeHealthResponse() { std::string quick_check_message; const bool quick_ok = sqliteQuickCheck(quick_check_message); std::ostringstream json; json << "{" << "\"status\":\"" << (quick_ok ? "healthy" : "degraded") << "\"," << "\"time\":\"" << jsonEscape(getCurrentTimestamp()) << "\"," << "\"storage\":\"sqlite\"," << "\"database\":\"" << jsonEscape(SQLITE_DB_FILE) << "\"," << "\"database_ok\":" << (quick_ok ? "true" : "false") << "," << "\"quick_check\":\"" << jsonEscape(quick_check_message) << "\"," << "\"sync_version\":" << readSyncVersion() << "}"; return json.str(); } // ============= 检查重复签到 ============= // 参数: student_id - 学号, slot_start - 时段开始时间(HH:MM), slot_end - 时段结束时间(HH:MM) // 返回: JSON格式 {"duplicate": true/false, "record_time": "..."} std::string checkDuplicate(const std::string& student_id, const std::string& slot_start, const std::string& slot_end) { std::string todayDate = getCurrentTimestamp().substr(0, 10); int start_hour = 0, start_minute = 0, end_hour = 23, end_minute = 59; parseTimeParts(slot_start, start_hour, start_minute); parseTimeParts(slot_end, end_hour, end_minute); int start_total_minutes = start_hour * 60 + start_minute; int end_total_minutes = end_hour * 60 + end_minute; sqlite3* db = openSqliteDb(); if (!db) return "{\"duplicate\":false}"; sqlite3_stmt* stmt = nullptr; const char* sql = "SELECT timestamp FROM records WHERE student_id=? AND substr(timestamp,1,10)=? ORDER BY id DESC"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, student_id); sqliteBindText(stmt, 2, todayDate); while (sqlite3_step(stmt) == SQLITE_ROW) { std::string timestamp = sqliteColumnText(stmt, 0); if (timestamp.length() < 16) continue; int record_hour = 0; int record_minute = 0; if (!parseIntInRange(timestamp.substr(11, 2), 0, 23, record_hour) || !parseIntInRange(timestamp.substr(14, 2), 0, 59, record_minute)) continue; int record_total_minutes = record_hour * 60 + record_minute; if (record_total_minutes >= start_total_minutes && record_total_minutes <= end_total_minutes) { sqlite3_finalize(stmt); sqlite3_close(db); std::cout << "[CHECK] Duplicate found: " << student_id << " at " << timestamp << std::endl; return "{\"duplicate\":true,\"record_time\":\"" + jsonEscape(timestamp) + "\"}"; } } } sqlite3_finalize(stmt); sqlite3_close(db); return "{\"duplicate\":false}"; } // ============= 删除记录 ============= bool deleteRecord(const std::string& timestamp, const std::string& student_id) { sqlite3* db = openSqliteDb(); if (!db) return false; sqlite3_stmt* stmt = nullptr; const char* sql = "DELETE FROM records WHERE timestamp=? AND student_id=?"; bool ok = false; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, timestamp); sqliteBindText(stmt, 2, student_id); ok = sqlite3_step(stmt) == SQLITE_DONE && sqlite3_changes(db) > 0; } sqlite3_finalize(stmt); sqlite3_close(db); return ok; } // ============= 人脸照片管理函数 ============= // 保存人脸ID映射配置 bool saveFaceIdMapping(const std::string& json_data) { std::cout << "[FACE_MAPPING] Saving " << json_data.size() << " bytes" << std::endl; std::map mappings = parseFaceIdMappings(json_data); sqlite3* db = openSqliteDb(); if (!db) return false; bool ok = sqliteExec(db, "BEGIN") && sqliteExec(db, "DELETE FROM face_mappings"); sqlite3_stmt* stmt = nullptr; const char* sql = "REPLACE INTO face_mappings(face_id,student_id) VALUES(?,?)"; if (ok && sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) { for (const auto& m : mappings) { sqlite3_reset(stmt); sqlite3_clear_bindings(stmt); sqliteBindText(stmt, 1, m.first); sqliteBindText(stmt, 2, m.second); if (sqlite3_step(stmt) != SQLITE_DONE) { ok = false; break; } } } else { ok = false; } sqlite3_finalize(stmt); sqliteExec(db, ok ? "COMMIT" : "ROLLBACK"); sqlite3_close(db); return ok; } std::map parseFaceIdMappings(const std::string& json_data) { // 解析JSON: {"mappings":{"1":"20210001","2":"20210002"}} std::map mappings; size_t mappings_start = json_data.find("\"mappings\":{"); if (mappings_start == std::string::npos) return mappings; mappings_start += 12; // 跳过 "mappings":{ size_t mappings_end = json_data.find("}", mappings_start); if (mappings_end == std::string::npos) return mappings; std::string mappings_str = json_data.substr(mappings_start, mappings_end - mappings_start); // 解析每个映射对 size_t pos = 0; while (pos < mappings_str.length()) { // 找到键 size_t key_start = mappings_str.find("\"", pos); if (key_start == std::string::npos) break; size_t key_end = mappings_str.find("\"", key_start + 1); if (key_end == std::string::npos) break; // 找到值 size_t val_start = mappings_str.find("\"", key_end + 1); if (val_start == std::string::npos) break; size_t val_end = mappings_str.find("\"", val_start + 1); if (val_end == std::string::npos) break; std::string key = mappings_str.substr(key_start + 1, key_end - key_start - 1); std::string value = mappings_str.substr(val_start + 1, val_end - val_start - 1); if (!key.empty() && !value.empty()) { mappings[key] = value; } pos = val_end + 1; } return mappings; } std::map loadFaceIdMappings() { std::map mappings; sqlite3* db = openSqliteDb(); if (!db) return mappings; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "SELECT face_id,student_id FROM face_mappings ORDER BY face_id", -1, &stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { mappings[sqliteColumnText(stmt, 0)] = sqliteColumnText(stmt, 1); } } sqlite3_finalize(stmt); sqlite3_close(db); return mappings; } std::set resolveK210FaceIdsForDelete(const std::string& requested_id) { std::set ids; bool matched_mapping = false; const auto mappings = loadFaceIdMappings(); for (const auto& m : mappings) { if (m.first == requested_id || m.second == requested_id) { ids.insert(m.first); matched_mapping = true; } } if (!matched_mapping) { ids.insert(requested_id); } return ids; } std::set resolveStudentIdsForDelete(const std::string& requested_id) { std::set ids; ids.insert(requested_id); const auto mappings = loadFaceIdMappings(); for (const auto& m : mappings) { if (m.first == requested_id || m.second == requested_id) { ids.insert(m.second); } } return ids; } // 根据映射重命名照片文件 void renameFacePhotos(const std::string& json_data) { std::cout << "[FACE_PHOTO] Renaming photos based on mapping..." << std::endl; const std::map mappings = parseFaceIdMappings(json_data); // 遍历faces目录,重命名匹配的照片 std::vector> rename_list; #ifdef _WIN32 WIN32_FIND_DATAA findData; std::string searchPath = FACES_DIR + "\\*.jpg"; HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData); if (hFind != INVALID_HANDLE_VALUE) { do { std::string filename = findData.cFileName; std::string base_name = filename.substr(0, filename.length() - 4); // 去掉.jpg // 检查是否以某个face_id开头 for (const auto& m : mappings) { // 匹配 "face_id.jpg" 或 "face_id_xxx.jpg" if (base_name == m.first || (base_name.find(m.first + "_") == 0)) { // 直接使用学号作为新文件名(不保留时间戳) std::string new_filename = m.second + ".jpg"; if (filename != new_filename) { rename_list.push_back({filename, new_filename}); } break; } } } while (FindNextFileA(hFind, &findData)); FindClose(hFind); } #else DIR* dir = opendir(FACES_DIR.c_str()); if (dir) { struct dirent* entry; while ((entry = readdir(dir)) != NULL) { std::string filename = entry->d_name; if (filename.size() > 4 && filename.substr(filename.size() - 4) == ".jpg") { std::string base_name = filename.substr(0, filename.length() - 4); for (const auto& m : mappings) { if (base_name == m.first || (base_name.find(m.first + "_") == 0)) { std::string new_filename = m.second + ".jpg"; if (filename != new_filename) { rename_list.push_back({filename, new_filename}); } break; } } } } closedir(dir); } #endif // 执行重命名 for (const auto& r : rename_list) { std::string old_id = r.first.substr(0, r.first.size() - 4); std::string new_id = r.second.substr(0, r.second.size() - 4); std::string old_path; std::string new_path; if (!buildFacePhotoPath(old_id, old_path) || !buildFacePhotoPath(new_id, new_path)) { std::cerr << "[FACE_PHOTO] Skip unsafe rename: " << r.first << " -> " << r.second << std::endl; continue; } std::remove(new_path.c_str()); if (std::rename(old_path.c_str(), new_path.c_str()) == 0) { std::cout << "[FACE_PHOTO] Renamed: " << r.first << " -> " << r.second << std::endl; } else { std::cerr << "[FACE_PHOTO] Rename failed: " << r.first << std::endl; } } std::cout << "[FACE_PHOTO] Renamed " << rename_list.size() << " photos" << std::endl; } // 读取人脸ID映射配置 std::string readFaceIdMapping() { std::map mappings = loadFaceIdMappings(); std::ostringstream json; json << "{\"mappings\":{"; bool first = true; for (const auto& m : mappings) { if (!first) json << ","; json << "\"" << jsonEscape(m.first) << "\":\"" << jsonEscape(m.second) << "\""; first = false; } json << "}}"; return json.str(); } // 获取人脸照片列表 std::string getFacePhotosList() { std::ostringstream json; json << "{\"photos\":["; std::vector photos; #ifdef _WIN32 WIN32_FIND_DATAA findData; std::string searchPath = FACES_DIR + "\\*.jpg"; HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData); if (hFind != INVALID_HANDLE_VALUE) { do { photos.push_back(findData.cFileName); } while (FindNextFileA(hFind, &findData)); FindClose(hFind); } #else DIR* dir = opendir(FACES_DIR.c_str()); if (dir) { struct dirent* entry; while ((entry = readdir(dir)) != NULL) { std::string name = entry->d_name; if (name.size() > 4 && name.substr(name.size() - 4) == ".jpg") { photos.push_back(name); } } closedir(dir); } #endif std::sort(photos.begin(), photos.end()); for (size_t i = 0; i < photos.size(); ++i) { if (i > 0) json << ","; std::string id = photos[i].substr(0, photos[i].size() - 4); json << "{\"id\":\"" << jsonEscape(id) << "\",\"filename\":\"" << jsonEscape(photos[i]) << "\"}"; } json << "]}"; return json.str(); } // 保存人脸照片 bool saveFacePhoto(const std::string& face_id, const std::string& photo_data) { _mkdir(FACES_DIR.c_str()); std::string filepath; if (!buildFacePhotoPath(face_id, filepath)) { std::cerr << "[FACE_PHOTO] Invalid face_id: " << face_id << std::endl; return false; } std::ofstream file(filepath, std::ios::binary); if (!file.is_open()) { std::cerr << "[FACE_PHOTO] Failed to create: " << filepath << std::endl; return false; } file.write(photo_data.c_str(), photo_data.size()); file.close(); std::cout << "[FACE_PHOTO] Saved: " << filepath << " (" << photo_data.size() << " bytes)" << std::endl; return true; } // 读取人脸照片 std::string readFacePhoto(const std::string& face_id) { std::string filepath; if (!buildFacePhotoPath(face_id, filepath)) return ""; std::ifstream file(filepath, std::ios::binary); if (!file.is_open()) return ""; std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); return content; } // 删除人脸照片 bool deleteFacePhoto(const std::string& face_id) { std::string filepath; if (!buildFacePhotoPath(face_id, filepath)) return false; if (std::remove(filepath.c_str()) == 0) { std::cout << "[FACE_PHOTO] Deleted: " << filepath << std::endl; return true; } return false; } // 从人脸ID映射中删除指定ID bool removeFaceIdFromMapping(const std::string& face_id) { sqlite3* db = openSqliteDb(); if (!db) return false; sqlite3_stmt* stmt = nullptr; bool ok = false; if (sqlite3_prepare_v2(db, "DELETE FROM face_mappings WHERE face_id=?", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, face_id); ok = sqlite3_step(stmt) == SQLITE_DONE && sqlite3_changes(db) > 0; } sqlite3_finalize(stmt); sqlite3_close(db); if (ok) std::cout << "[FACE_MAPPING] Removed face_id: " << face_id << std::endl; return ok; } // 删除指定学生ID的所有考勤记录 int deleteRecordsByStudentId(const std::string& student_id) { sqlite3* db = openSqliteDb(); if (!db) return 0; sqlite3_stmt* stmt = nullptr; int deleted_count = 0; if (sqlite3_prepare_v2(db, "DELETE FROM records WHERE student_id=?", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, student_id); if (sqlite3_step(stmt) == SQLITE_DONE) { deleted_count = sqlite3_changes(db); } } sqlite3_finalize(stmt); sqlite3_close(db); return deleted_count; } // 获取所有学生ID(从考勤记录提取,包括没有照片的) std::string getAllStudentIds() { std::set student_ids; sqlite3* db = openSqliteDb(); if (db) { sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "SELECT DISTINCT student_id FROM records WHERE student_id<>'' ORDER BY student_id", -1, &stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { student_ids.insert(sqliteColumnText(stmt, 0)); } } sqlite3_finalize(stmt); sqlite3_close(db); } // 构建JSON响应 std::ostringstream json; json << "{\"student_ids\":["; bool first = true; for (const auto& id : student_ids) { if (!first) json << ","; json << "\"" << jsonEscape(id) << "\""; first = false; } json << "]}"; return json.str(); } bool updateDeviceSyncState(const std::string& client, unsigned long long seen_version, unsigned long long ack_version) { if (client.empty()) return false; sqlite3* db = openSqliteDb(); if (!db) return false; sqlite3_stmt* stmt = nullptr; bool ok = false; const char* sql = "INSERT INTO device_sync_state(client,last_seen_version,last_ack_version,last_seen_at) " "VALUES(?,?,?,?) " "ON CONFLICT(client) DO UPDATE SET " "last_seen_version=MAX(device_sync_state.last_seen_version, excluded.last_seen_version)," "last_ack_version=MAX(device_sync_state.last_ack_version, excluded.last_ack_version)," "last_seen_at=excluded.last_seen_at"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, client); sqlite3_bind_int64(stmt, 2, static_cast(seen_version)); sqlite3_bind_int64(stmt, 3, static_cast(ack_version)); sqliteBindText(stmt, 4, getCurrentTimestamp()); ok = sqlite3_step(stmt) == SQLITE_DONE; } sqlite3_finalize(stmt); sqlite3_close(db); return ok; } std::string getDeviceSyncSummary() { sqlite3* db = openSqliteDb(); if (!db) return "[]"; sqlite3_stmt* stmt = nullptr; std::ostringstream json; json << "["; bool first = true; const char* sql = "SELECT client,last_seen_version,last_ack_version,last_seen_at FROM device_sync_state ORDER BY client"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { if (!first) json << ","; json << "{" << "\"client\":\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\"," << "\"last_seen_version\":" << sqlite3_column_int64(stmt, 1) << "," << "\"last_ack_version\":" << sqlite3_column_int64(stmt, 2) << "," << "\"last_seen_at\":\"" << jsonEscape(sqliteColumnText(stmt, 3)) << "\"" << "}"; first = false; } } sqlite3_finalize(stmt); sqlite3_close(db); json << "]"; return json.str(); } // 添加人脸ID到待删除列表 bool addToPendingDelete(const std::string& face_id, const std::string& student_id) { sqlite3* db = openSqliteDb(); if (!db) return false; sqlite3_stmt* stmt = nullptr; bool ok = false; std::string task_id = face_id + "-" + std::to_string(static_cast(time(0))); if (sqlite3_prepare_v2(db, "REPLACE INTO pending_delete(task_id,face_id,student_id,created_at,status) VALUES(?,?,?,?,?)", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, task_id); sqliteBindText(stmt, 2, face_id); sqliteBindText(stmt, 3, student_id); sqliteBindText(stmt, 4, getCurrentTimestamp()); sqliteBindText(stmt, 5, "pending"); ok = sqlite3_step(stmt) == SQLITE_DONE; } sqlite3_finalize(stmt); sqlite3_close(db); std::cout << "[PENDING_DELETE] Added: " << face_id << " task=" << task_id << std::endl; return ok; } // 获取待删除列表 std::string getPendingDeleteList() { sqlite3* db = openSqliteDb(); if (!db) return "{\"pending_delete\":[],\"delete_tasks\":[]}"; sqlite3_stmt* stmt = nullptr; std::vector> tasks; if (sqlite3_prepare_v2(db, "SELECT task_id,face_id,student_id,created_at,status,COALESCE(ack_at,'') FROM pending_delete WHERE status<>'acked' ORDER BY created_at,face_id", -1, &stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { std::map row; row["task_id"] = sqliteColumnText(stmt, 0); row["face_id"] = sqliteColumnText(stmt, 1); row["student_id"] = sqliteColumnText(stmt, 2); row["created_at"] = sqliteColumnText(stmt, 3); row["status"] = sqliteColumnText(stmt, 4); row["ack_at"] = sqliteColumnText(stmt, 5); tasks.push_back(row); } } sqlite3_finalize(stmt); sqlite3_close(db); std::ostringstream json; json << "{\"pending_delete\":["; bool first = true; for (const auto& task : tasks) { if (!first) json << ","; json << "\"" << jsonEscape(task.at("face_id")) << "\""; first = false; } json << "],\"delete_tasks\":["; first = true; for (const auto& task : tasks) { if (!first) json << ","; json << "{" << "\"task_id\":\"" << jsonEscape(task.at("task_id")) << "\"," << "\"face_id\":\"" << jsonEscape(task.at("face_id")) << "\"," << "\"student_id\":\"" << jsonEscape(task.at("student_id")) << "\"," << "\"created_at\":\"" << jsonEscape(task.at("created_at")) << "\"," << "\"status\":\"" << jsonEscape(task.at("status")) << "\"" << "}"; first = false; } json << "]}"; return json.str(); } // 从待删除列表中移除指定ID(K210确认删除后调用) bool removeFromPendingDelete(const std::string& face_id_or_task_id) { sqlite3* db = openSqliteDb(); if (!db) return false; sqlite3_stmt* stmt = nullptr; bool ok = false; if (sqlite3_prepare_v2(db, "UPDATE pending_delete SET status='acked',ack_at=? WHERE (task_id=? OR face_id=?) AND status<>'acked'", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, getCurrentTimestamp()); sqliteBindText(stmt, 2, face_id_or_task_id); sqliteBindText(stmt, 3, face_id_or_task_id); ok = sqlite3_step(stmt) == SQLITE_DONE && sqlite3_changes(db) > 0; } sqlite3_finalize(stmt); sqlite3_close(db); std::cout << "[PENDING_DELETE] Acked: " << face_id_or_task_id << std::endl; return ok; } // ============= K210同步请求管理 ============= // 设置同步请求标志(QT端调用) bool setSyncRequest(bool sync) { if (!sqliteSetKvValue("sync_request", std::string("{\"sync\":") + (sync ? "true" : "false") + "}")) { return false; } std::cout << "[SYNC] Sync request set to: " << (sync ? "true" : "false") << std::endl; return true; } // 获取同步请求状态(K210轮询调用) std::string getSyncRequest() { return sqliteGetKvValue("sync_request", "{\"sync\":false}"); } // 清除同步请求标志(K210确认后调用) bool clearSyncRequest() { return setSyncRequest(false); } // 根据K210的face_id查询真实学号 std::string getMappedStudentId(const std::string& face_id) { std::cout << "[MAPPING] Looking up face_id: " << face_id << std::endl; const auto mappings = loadFaceIdMappings(); auto it = mappings.find(face_id); if (it != mappings.end() && !it->second.empty()) { std::cout << "[MAPPING] ✓ Face ID " << face_id << " -> Student ID " << it->second << std::endl; return it->second; } std::cout << "[MAPPING] No mapping found for face_id: " << face_id << std::endl; return face_id; // 没找到映射,返回原ID } // ============= HTTP请求处理 ============= void handleHttpRequest(SOCKET client_socket) { char buffer[BUFFER_SIZE] = {0}; int bytes = recv(client_socket, buffer, BUFFER_SIZE - 1, 0); if (bytes <= 0) { closesocket(client_socket); return; } std::string request(buffer, bytes); std::istringstream stream(request); std::string method, path, version; stream >> method >> path >> version; std::cout << "[HTTP] " << method << " " << path << std::endl; std::string response_body; std::string content_type = "application/json"; int status_code = 200; std::string status_text = "OK"; auto sendResponse = [&]() { std::ostringstream response; response << "HTTP/1.1 " << status_code << " " << status_text << "\r\n"; response << "Content-Type: " << content_type << "; charset=utf-8\r\n"; response << "Content-Length: " << response_body.length() << "\r\n"; response << "Access-Control-Allow-Origin: " << allowedCorsOrigin() << "\r\n"; response << "Access-Control-Allow-Headers: Content-Type, X-Api-Token, X-Encrypted-Response, X-Encrypted\r\n"; response << "Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS\r\n"; response << "Connection: close\r\n"; response << "\r\n"; response << response_body; std::string resp_str = response.str(); send(client_socket, resp_str.c_str(), resp_str.length(), 0); closesocket(client_socket); }; std::string endpoint = path; std::string query; size_t q_pos = path.find('?'); if (q_pos != std::string::npos) { endpoint = path.substr(0, q_pos); query = path.substr(q_pos + 1); } const std::string original_endpoint = endpoint; endpoint = normalizeApiEndpoint(method, endpoint); if (endpoint != original_endpoint) { std::cout << "[HTTP] Route alias: " << original_endpoint << " -> " << endpoint << std::endl; } if (!isAuthorizedRequest(request, method, endpoint)) { response_body = makeJsonError("Unauthorized"); status_code = 401; status_text = "Unauthorized"; sendResponse(); return; } // 提取请求体 std::string request_body; if (method == "POST") { size_t content_length = 0; std::string cl_str; if (getHeaderValue(request, "Content-Length", cl_str)) { int parsed_content_length = 0; if (!parseIntInRange(cl_str, 0, static_cast(MAX_PHOTO_BODY_SIZE), parsed_content_length)) { response_body = makeJsonError("Invalid Content-Length"); status_code = 400; status_text = "Bad Request"; sendResponse(); return; } content_length = static_cast(parsed_content_length); } const bool is_photo_upload = endpoint.find("/api/face_photo") == 0; const size_t max_body_size = is_photo_upload ? MAX_PHOTO_BODY_SIZE : MAX_JSON_BODY_SIZE; if (content_length > max_body_size) { response_body = makeJsonError("Request body too large"); status_code = 413; status_text = "Payload Too Large"; sendResponse(); return; } size_t header_end = request.find("\r\n\r\n"); if (header_end != std::string::npos) { size_t body_start = header_end + 4; size_t current_body_size = request.size() - body_start; if (current_body_size >= content_length) { request_body = request.substr(body_start, content_length); } else { request_body = request.substr(body_start); size_t remaining = content_length - current_body_size; while (remaining > 0) { char extra_buffer[BUFFER_SIZE] = {0}; int extra_bytes = recv(client_socket, extra_buffer, static_cast(std::min(remaining, (size_t)BUFFER_SIZE)), 0); if (extra_bytes <= 0) break; request_body.append(extra_buffer, extra_bytes); remaining -= extra_bytes; } } } std::cout << "[HTTP] POST body size: " << request_body.size() << " bytes (expected: " << content_length << ")" << std::endl; if (request_body.size() != content_length) { response_body = makeJsonError("Incomplete request body"); status_code = 400; status_text = "Bad Request"; sendResponse(); return; } } // ============= 路由处理 ============= if (method == "OPTIONS") { response_body = "{}"; } else if (method == "GET" && endpoint == "/api/v1/sync/version") { response_body = makeSyncVersionResponse(); std::cout << "[SYNC] Version fetched: " << readSyncVersion() << std::endl; } else if (method == "GET" && (endpoint == "/api/sync/state" || endpoint == "/api/v1/sync/state")) { auto params = parseQueryParams(query); const std::string client = params.count("client") ? params["client"] : "all"; const unsigned long long last_version = parseVersionParam(params); response_body = makeSyncStateResponse(client, last_version); std::cout << "[SYNC] State fetched: client=" << client << ", last_version=" << last_version << ", current_version=" << readSyncVersion() << std::endl; } else if (method == "POST" && (endpoint == "/api/sync/ack" || endpoint == "/api/v1/sync/ack")) { response_body = makeSyncAckResponse(request_body); std::cout << "[SYNC] Ack received: " << request_body << std::endl; // 新增:处理加密签到数据 } else if (method == "POST" && endpoint == "/api/checkin") { std::cout << "[CHECKIN] Processing encrypted checkin data..." << std::endl; // 检查是否是加密数据 if (request.find("X-Encrypted: AES128-CBC") != std::string::npos) { std::cout << "[CHECKIN] Encrypted data detected" << std::endl; // 解密数据 std::string decrypted_json = decryptJSON(request_body); if (decrypted_json.empty()) { response_body = "{\"success\":false,\"message\":\"解密失败\"}"; status_code = 400; status_text = "Bad Request"; } else { // 解析JSON auto json_data = parseJSON(decrypted_json); if (json_data.find("student_id") != json_data.end()) { // 构建考勤记录 std::string timestamp = getCurrentTimestamp(); std::string student_id = json_data["student_id"]; // K210已发送真实学号 std::string name = json_data.count("name") ? json_data["name"] : ""; std::string confidence = json_data.count("confidence") ? json_data["confidence"] : "0"; std::string status = json_data.count("status") ? json_data["status"] : "success"; std::string class_id = json_data.count("class_id") ? json_data["class_id"] : "0"; char* conf_end = nullptr; const double conf_value = std::strtod(confidence.c_str(), &conf_end); if (!isSafeId(student_id) || student_id.size() > 32 || name.size() > 64 || class_id.size() > 32 || conf_end == confidence.c_str() || *conf_end != '\0' || conf_value < 0.0 || conf_value > 1.0 || (status != "success" && status != "failed" && status != "late")) { response_body = makeJsonError("Invalid checkin fields"); status_code = 400; status_text = "Bad Request"; sendResponse(); return; } std::cout << "[CHECKIN] Student ID: " << student_id << std::endl; // 保存到SQLite(包含班级编号) if (saveAttendanceRecord(timestamp, student_id, name, confidence, status, class_id)) { response_body = "{\"success\":true,\"message\":\"签到成功\",\"timestamp\":\"" + jsonEscape(timestamp) + "\"}"; bumpSyncVersion("checkin"); std::cout << "[CHECKIN] ✓ " << name << " (ID:" << student_id << ", Class:" << class_id << ")" << std::endl; } else { response_body = "{\"success\":false,\"message\":\"数据库写入失败\"}"; status_code = 500; status_text = "Internal Server Error"; } } else { response_body = "{\"success\":false,\"message\":\"缺少必要字段\"}"; status_code = 400; status_text = "Bad Request"; } } } else { response_body = "{\"success\":false,\"message\":\"未加密数据\"}"; status_code = 400; status_text = "Bad Request"; } } // API端点:返回加密数据 else if (method == "GET" && endpoint == "/api/records") { auto params = parseQueryParams(query); int limit = 100; int offset = 0; if (params.count("limit") && !parseIntInRange(params["limit"], 1, 5000, limit)) { response_body = makeJsonError("Invalid limit"); status_code = 400; status_text = "Bad Request"; } else if (params.count("offset") && !parseIntInRange(params["offset"], 0, 1000000, offset)) { response_body = makeJsonError("Invalid offset"); status_code = 400; status_text = "Bad Request"; } else { std::string json_data = readDatabase(limit, offset); if (hasHeaderToken(request, "X-Encrypted-Response", "true")) { response_body = encryptJSON(json_data); if (response_body.empty()) { response_body = makeJsonError("Encryption failed"); status_code = 500; status_text = "Internal Server Error"; } } else { response_body = json_data; } } } else if (method == "GET" && endpoint == "/api/statistics") { std::string json_data = getStatistics(); if (hasHeaderToken(request, "X-Encrypted-Response", "true")) { response_body = encryptJSON(json_data); if (response_body.empty()) { response_body = makeJsonError("Encryption failed"); status_code = 500; status_text = "Internal Server Error"; } } else { response_body = json_data; } } else if (method == "POST" && endpoint == "/api/clear") { if (clearDatabase()) { response_body = "{\"success\":true,\"message\":\"Database cleared\"}"; bumpSyncVersion("clear_records"); } else { response_body = "{\"success\":false,\"message\":\"Failed to clear database\"}"; status_code = 500; status_text = "Internal Server Error"; } } else if (method == "DELETE" && endpoint == "/api/record") { auto params = parseQueryParams(query); if (params.count("timestamp") && params.count("student_id")) { std::string timestamp = params["timestamp"]; std::string student_id = params["student_id"]; if (deleteRecord(timestamp, student_id)) { response_body = "{\"success\":true,\"message\":\"Record deleted\"}"; bumpSyncVersion("delete_record"); } else { response_body = "{\"success\":false,\"message\":\"Record not found\"}"; status_code = 404; status_text = "Not Found"; } } else { response_body = "{\"success\":false,\"message\":\"Missing parameters\"}"; status_code = 400; status_text = "Bad Request"; } } else if (method == "GET" && endpoint == "/api/health") { response_body = makeHealthResponse(); } else if (method == "GET" && endpoint == "/api/v1/system/storage") { response_body = makeStorageStatusResponse(); } else if (method == "GET" && endpoint == "/api/time") { // 返回服务器当前Unix时间戳(UTC秒)和可读时间 std::ostringstream json; json << "{" << "\"epoch\":" << static_cast(time(0)) << "," << "\"server_time\":\"" << getCurrentTimestamp() << "\"" << "}"; response_body = json.str(); } else if (method == "POST" && endpoint == "/api/timeslots") { // 上传时间段配置: POST /api/timeslots std::cout << "[TIMESLOTS] Received upload request, data size: " << request_body.size() << " bytes" << std::endl; if (saveTimeSlots(request_body)) { response_body = "{\"success\":true,\"message\":\"时间段配置已更新\"}"; bumpSyncVersion("timeslots"); std::cout << "[TIMESLOTS] ✓ Configuration uploaded successfully" << std::endl; } else { response_body = "{\"success\":false,\"message\":\"保存时间段配置失败\"}"; status_code = 500; status_text = "Internal Server Error"; } } else if (method == "GET" && endpoint == "/api/timeslots") { // 获取时间段配置: GET /api/timeslots std::cout << "[TIMESLOTS] Reading configuration" << std::endl; response_body = readTimeSlots(); } else if (method == "GET" && endpoint == "/api/check_duplicate") { // 检查重复签到: GET /api/check_duplicate?student_id=xxx&slot_start=08:00&slot_end=09:30 auto params = parseQueryParams(query); if (params.count("student_id") && params.count("slot_start") && params.count("slot_end")) { response_body = checkDuplicate(params["student_id"], params["slot_start"], params["slot_end"]); std::cout << "[CHECK] student_id=" << params["student_id"] << ", slot=" << params["slot_start"] << "-" << params["slot_end"] << ", result=" << response_body << std::endl; } else { response_body = "{\"error\":\"Missing parameters: student_id, slot_start, slot_end\"}"; status_code = 400; status_text = "Bad Request"; } } else if (method == "GET" && endpoint == "/api/today_records") { // 获取今日已签到记录(供STM32开机下载) // GET /api/today_records?slot_start=08:00&slot_end=09:30 auto params = parseQueryParams(query); std::string slot_start = params.count("slot_start") ? params["slot_start"] : "00:00"; std::string slot_end = params.count("slot_end") ? params["slot_end"] : "23:59"; std::string todayDate = getCurrentTimestamp().substr(0, 10); int start_hour = 0, start_minute = 0, end_hour = 23, end_minute = 59; parseTimeParts(slot_start, start_hour, start_minute); parseTimeParts(slot_end, end_hour, end_minute); std::ostringstream json; json << "{\"date\":\"" << jsonEscape(todayDate) << "\",\"records\":["; sqlite3* db = openSqliteDb(); if (db) { sqlite3_stmt* stmt = nullptr; bool first = true; const char* sql = "SELECT timestamp,student_id FROM records WHERE substr(timestamp,1,10)=? ORDER BY id ASC"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, todayDate); while (sqlite3_step(stmt) == SQLITE_ROW) { std::string timestamp = sqliteColumnText(stmt, 0); if (timestamp.length() < 16) continue; int record_hour = 0; int record_minute = 0; if (!parseIntInRange(timestamp.substr(11, 2), 0, 23, record_hour) || !parseIntInRange(timestamp.substr(14, 2), 0, 59, record_minute)) continue; int record_total = record_hour * 60 + record_minute; int start_total = start_hour * 60 + start_minute; int end_total = end_hour * 60 + end_minute; if (record_total < start_total || record_total > end_total) continue; if (!first) json << ","; json << "\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\""; first = false; } } sqlite3_finalize(stmt); sqlite3_close(db); } json << "]}"; response_body = json.str(); std::cout << "[TODAY] Returning today's records: " << response_body << std::endl; } else if (method == "POST" && endpoint == "/api/mapping_config") { // 上传映射配置: POST /api/mapping_config std::cout << "[MAPPING] Received upload request, data size: " << request_body.size() << " bytes" << std::endl; if (saveMappingConfig(request_body)) { response_body = "{\"success\":true,\"message\":\"映射配置已保存\"}"; bumpSyncVersion("mapping_config"); std::cout << "[MAPPING] ✓ Configuration saved successfully" << std::endl; } else { response_body = "{\"success\":false,\"message\":\"保存映射配置失败\"}"; status_code = 500; status_text = "Internal Server Error"; } } else if (method == "GET" && endpoint == "/api/mapping_config") { // 获取映射配置: GET /api/mapping_config std::cout << "[MAPPING] Reading configuration" << std::endl; response_body = readMappingConfig(); std::cout << "[MAPPING] Returning config: " << response_body.size() << " bytes" << std::endl; // ============= 人脸照片管理 API ============= } else if (method == "GET" && endpoint == "/api/face_photos") { // 获取人脸照片列表 _mkdir(FACES_DIR.c_str()); response_body = getFacePhotosList(); std::cout << "[FACE_PHOTO] Photos list fetched" << std::endl; } else if (method == "GET" && endpoint.find("/api/face_photo/") == 0) { // 获取指定人脸照片: GET /api/face_photo/ std::string face_id = urlDecode(endpoint.substr(16)); std::string photo_data = readFacePhoto(face_id); if (!photo_data.empty()) { response_body = photo_data; content_type = "image/jpeg"; std::cout << "[FACE_PHOTO] Photo fetched for id: " << face_id << std::endl; } else { response_body = "{\"error\":\"Photo not found\"}"; status_code = 404; status_text = "Not Found"; } } else if (method == "POST" && endpoint.find("/api/face_photo_simple/") == 0) { // 简化版人脸照片上传: POST /api/face_photo_simple/ std::string face_id = urlDecode(endpoint.substr(23)); std::cout << "[FACE_PHOTO] Simple upload for face_id: " << face_id << ", size: " << request_body.size() << " bytes" << std::endl; if (!isSafeId(face_id)) { response_body = "{\"success\":false,\"message\":\"Invalid face_id\"}"; status_code = 400; status_text = "Bad Request"; } else if (!request_body.empty()) { if (saveFacePhoto(face_id, request_body)) { response_body = "{\"success\":true,\"message\":\"Photo saved\",\"face_id\":\"" + jsonEscape(face_id) + "\"}"; bumpSyncVersion("face_photo"); } else { response_body = "{\"success\":false,\"message\":\"Failed to save photo\"}"; status_code = 500; status_text = "Internal Server Error"; } } else { response_body = "{\"success\":false,\"message\":\"Invalid face_id or empty data\"}"; status_code = 400; status_text = "Bad Request"; } } else if (method == "POST" && endpoint == "/api/face_id_mapping") { // 保存人脸ID映射 std::cout << "[FACE_MAPPING] Saving mapping, size: " << request_body.size() << " bytes" << std::endl; if (saveFaceIdMapping(request_body)) { // 根据新映射重命名照片文件 renameFacePhotos(request_body); // 自动触发K210同步 bumpSyncVersion("face_id_mapping"); setSyncRequest(true); std::cout << "[FACE_MAPPING] Auto-triggered K210 sync" << std::endl; response_body = "{\"success\":true,\"message\":\"Face ID mapping saved, K210 sync triggered\"}"; } else { response_body = "{\"success\":false,\"message\":\"Failed to save mapping\"}"; status_code = 500; status_text = "Internal Server Error"; } } else if (method == "GET" && endpoint == "/api/face_id_mapping") { // 获取人脸ID映射 response_body = readFaceIdMapping(); std::cout << "[FACE_MAPPING] Mapping fetched" << std::endl; } else if (method == "GET" && endpoint == "/api/all_student_ids") { // 获取所有学生ID(从考勤记录提取) response_body = getAllStudentIds(); std::cout << "[STUDENT_IDS] All student IDs fetched" << std::endl; } else if (method == "DELETE" && endpoint.find("/api/face/") == 0) { // 删除人脸信息: DELETE /api/face/?delete_records=true std::string student_id = urlDecode(endpoint.substr(10)); // 去掉 "/api/face/" auto params = parseQueryParams(query); bool delete_records = (params.count("delete_records") && params["delete_records"] == "true"); std::cout << "[DELETE_FACE] Deleting face for student_id: " << student_id << ", delete_records: " << (delete_records ? "yes" : "no") << std::endl; if (!isSafeId(student_id)) { response_body = "{\"success\":false,\"message\":\"Invalid student_id\"}"; status_code = 400; status_text = "Bad Request"; } else { const std::set k210_face_ids = resolveK210FaceIdsForDelete(student_id); const std::set student_ids = resolveStudentIdsForDelete(student_id); bool photo_deleted = deleteFacePhoto(student_id); bool mapping_deleted = removeFaceIdFromMapping(student_id); int records_deleted = 0; if (delete_records) { for (const auto& id : student_ids) { records_deleted += deleteRecordsByStudentId(id); } } // 添加K210原始face_id到待删除列表,等待K210同步删除本地特征 bool pending_added = false; for (const auto& face_id : k210_face_ids) { if (face_id != student_id) { photo_deleted = deleteFacePhoto(face_id) || photo_deleted; mapping_deleted = removeFaceIdFromMapping(face_id) || mapping_deleted; } pending_added = addToPendingDelete(face_id, student_id) || pending_added; } for (const auto& id : student_ids) { photo_deleted = deleteFacePhoto(id) || photo_deleted; } bumpSyncVersion("delete_face"); setSyncRequest(true); std::ostringstream result; result << "{\"success\":true," << "\"photo_deleted\":" << (photo_deleted ? "true" : "false") << "," << "\"mapping_deleted\":" << (mapping_deleted ? "true" : "false") << "," << "\"records_deleted\":" << records_deleted << "," << "\"k210_pending\":" << (pending_added ? "true" : "false") << "," << "\"sync_requested\":true}"; response_body = result.str(); std::cout << "[DELETE_FACE] Result: " << response_body << std::endl; } } else if (method == "GET" && endpoint == "/api/pending_delete") { // K210查询待删除列表 response_body = getPendingDeleteList(); std::cout << "[PENDING_DELETE] List fetched" << std::endl; } else if (method == "POST" && endpoint == "/api/confirm_delete") { // K210确认删除完成,从待删除列表移除 // 请求体格式: {"face_id":"xxx","task_id":"xxx"} auto ack_json = parseJSON(request_body); std::string ack_id = ack_json.count("task_id") ? ack_json["task_id"] : ""; if (ack_id.empty() && ack_json.count("face_id")) { ack_id = ack_json["face_id"]; } if (!ack_id.empty() && removeFromPendingDelete(ack_id)) { response_body = "{\"success\":true,\"message\":\"Confirmed delete: " + jsonEscape(ack_id) + "\"}"; bumpSyncVersion("confirm_delete"); std::cout << "[PENDING_DELETE] K210 confirmed delete: " << ack_id << std::endl; } else { response_body = "{\"success\":false,\"message\":\"Invalid face_id or not in pending list\"}"; status_code = 400; status_text = "Bad Request"; } // ============= K210同步请求 API ============= } else if (method == "GET" && endpoint == "/api/sync_request") { // K210查询是否需要同步 response_body = getSyncRequest(); std::cout << "[SYNC] K210 checking sync request: " << response_body << std::endl; } else if (method == "POST" && endpoint == "/api/sync_ack") { // K210确认收到同步请求,清除标志 if (clearSyncRequest()) { response_body = "{\"success\":true,\"message\":\"Sync acknowledged\"}"; std::cout << "[SYNC] K210 acknowledged sync request" << std::endl; } else { response_body = "{\"success\":false,\"message\":\"Failed to clear sync request\"}"; status_code = 500; status_text = "Internal Server Error"; } } else if (method == "POST" && endpoint == "/api/trigger_sync") { // QT端触发K210同步 if (setSyncRequest(true)) { response_body = "{\"success\":true,\"message\":\"Sync request sent to K210\"}"; std::cout << "[SYNC] QT triggered K210 sync" << std::endl; } else { response_body = "{\"success\":false,\"message\":\"Failed to set sync request\"}"; status_code = 500; status_text = "Internal Server Error"; } } else { response_body = "{\"error\":\"Not Found\"}"; status_code = 404; status_text = "Not Found"; } sendResponse(); } // ============= HTTP服务器线程 ============= void httpServerThread() { SOCKET server_socket = socket(AF_INET, SOCK_STREAM, 0); if (server_socket == INVALID_SOCKET) { std::cerr << "[HTTP] Failed to create socket" << std::endl; return; } int opt = 1; setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt)); sockaddr_in server_addr; server_addr.sin_family = AF_INET; const std::string bind_host = bindAddress(); if (bind_host == "0.0.0.0" || bind_host.empty()) { server_addr.sin_addr.s_addr = INADDR_ANY; } else { if (inet_pton(AF_INET, bind_host.c_str(), &server_addr.sin_addr) != 1) { std::cerr << "[HTTP] Invalid ATTENDANCE_BIND_HOST: " << bind_host << std::endl; closesocket(server_socket); return; } } server_addr.sin_port = htons(HTTP_PORT); if (bind(server_socket, (sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) { std::cerr << "[HTTP] Bind failed" << std::endl; closesocket(server_socket); return; } listen(server_socket, 10); std::cout << "[HTTP] Server listening on " << bind_host << ":" << HTTP_PORT << std::endl; while (true) { sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); SOCKET client_socket = accept(server_socket, (sockaddr*)&client_addr, &client_len); if (client_socket != INVALID_SOCKET) { std::thread(handleHttpRequest, client_socket).detach(); } } } int main() { #ifdef _WIN32 WSADATA wsa; WSAStartup(MAKEWORD(2, 2), &wsa); #endif std::cout << "========================================" << std::endl; std::cout << " Face Recognition Attendance Server" << std::endl; std::cout << " with AES-128-CBC Encryption" << std::endl; std::cout << "========================================" << std::endl; std::cout << "HTTP Port " << HTTP_PORT << " - Encrypted API" << std::endl; std::cout << "Database: " << SQLITE_DB_FILE << std::endl; std::cout << "----------------------------------------" << std::endl; if (!backupSqliteDatabaseOnStartup()) { std::cerr << "[BACKUP] Startup backup failed, stop to avoid unsafe writes" << std::endl; return 1; } // 初始化SQLite,并首次导入旧CSV/JSON配置 if (!initializeSqliteStorage()) { std::cerr << "[SQLITE] Failed to initialize storage" << std::endl; return 1; } std::cout << "[SYNC] Current version: " << readSyncVersion() << std::endl; // 启动HTTP服务器 std::thread http_thread(httpServerThread); http_thread.join(); #ifdef _WIN32 WSACleanup(); #endif return 0; }