commit 62356e5117a58010ef96c7f43b52e2040fbd8bf8 Author: 纪田野 Date: Mon May 4 02:51:01 2026 +0800 Initial Server project upload diff --git a/regression_api_tests.ps1 b/regression_api_tests.ps1 new file mode 100644 index 0000000..3d95abc --- /dev/null +++ b/regression_api_tests.ps1 @@ -0,0 +1,62 @@ +param( + [string]$BaseUrl = "http://127.0.0.1:8082", + [string]$ApiToken = "atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV" +) + +$ErrorActionPreference = "Stop" +$headers = @{ + "X-Api-Token" = $ApiToken + "Accept" = "application/json" +} + +function Invoke-ApiCheck { + param( + [string]$Name, + [string]$Path, + [string]$Method = "GET", + [object]$Body = $null + ) + + $uri = "$BaseUrl$Path" + $requestHeaders = @{} + $headers + $params = @{ + Uri = $uri + Method = $Method + Headers = $requestHeaders + TimeoutSec = 8 + } + + if ($null -ne $Body) { + $params.ContentType = "application/json" + $params.Body = ($Body | ConvertTo-Json -Depth 10 -Compress) + } + + try { + $response = Invoke-RestMethod @params + Write-Host "[PASS] $Name" + return $response + } catch { + Write-Host "[FAIL] $Name - $($_.Exception.Message)" + throw + } +} + +Invoke-ApiCheck "health" "/api/v1/system/health" | Out-Null +Invoke-ApiCheck "storage" "/api/v1/system/storage" | Out-Null +Invoke-ApiCheck "time" "/api/v1/system/time" | Out-Null +Invoke-ApiCheck "sync version" "/api/v1/sync/version" | Out-Null +Invoke-ApiCheck "sync state k210" "/api/v1/sync/state?client=k210&last_version=0" | Out-Null +Invoke-ApiCheck "sync state stm32" "/api/v1/sync/state?client=stm32&last_version=0" | Out-Null +Invoke-ApiCheck "records" "/api/v1/attendance/records?limit=10&offset=0" | Out-Null +Invoke-ApiCheck "statistics" "/api/v1/attendance/statistics" | Out-Null +Invoke-ApiCheck "time slots" "/api/v1/config/time-slots" | Out-Null +Invoke-ApiCheck "duplicate check" "/api/v1/attendance/duplicates?student_id=1&slot_start=00:00&slot_end=23:59" | Out-Null +Invoke-ApiCheck "today records" "/api/v1/attendance/today?slot_start=00:00&slot_end=23:59" | Out-Null +Invoke-ApiCheck "mapping config" "/api/v1/config/mappings" | Out-Null +Invoke-ApiCheck "face photos" "/api/v1/faces/photos" | Out-Null +Invoke-ApiCheck "face mappings" "/api/v1/faces/mappings" | Out-Null +Invoke-ApiCheck "student ids" "/api/v1/faces/student-ids" | Out-Null +Invoke-ApiCheck "pending deletes" "/api/v1/faces/pending-delete" | Out-Null +Invoke-ApiCheck "sync request" "/api/v1/sync/request" | Out-Null + +Write-Host "All regression API checks passed." diff --git a/server_encrypted.cpp b/server_encrypted.cpp new file mode 100644 index 0000000..ac9962f --- /dev/null +++ b/server_encrypted.cpp @@ -0,0 +1,3066 @@ +/* + * 增强版考勤服务器 - 支持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 + #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 FACES_DIR = "faces"; // 人脸照片目录 +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\":[]}"; + +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::map parseSimpleStringMap(const std::string& json_data, const std::string& key); +std::string extractJsonMemberValue(const std::string& json, const std::string& key, const std::string& fallback); + +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; +} + +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 hasBearerToken(const std::string& request, const std::string& expected) { + std::string authorization; + if (!getHeaderValue(request, "Authorization", authorization)) { + return false; + } + + const std::string prefix = "Bearer "; + if (authorization.size() <= prefix.size()) { + return false; + } + + std::string scheme = authorization.substr(0, prefix.size()); + std::transform(scheme.begin(), scheme.end(), scheme.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (scheme != "bearer ") { + return false; + } + + return authorization.substr(prefix.size()) == expected; +} + +bool isPublicEndpoint(const std::string& method, const std::string& endpoint) { + return method == "OPTIONS" || (method == "GET" && endpoint == "/api/v1/system/health"); +} + +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()) || hasBearerToken(request, 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; +} + +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; +} + +std::string normalizedClassId(const std::string& class_id) { + return class_id.empty() ? "0" : class_id; +} + +std::string defaultClassName(const std::string& class_id) { + return class_id == "0" ? "默认班级" : class_id; +} + +bool upsertClass(sqlite3* db, const std::string& class_id, const std::string& class_name, const std::string& now) { + if (!db) return false; + const std::string normalized_id = normalizedClassId(class_id); + const std::string normalized_name = class_name.empty() ? defaultClassName(normalized_id) : class_name; + + sqlite3_stmt* stmt = nullptr; + const char* sql = + "INSERT INTO classes(class_id,class_name,updated_at) VALUES(?,?,?) " + "ON CONFLICT(class_id) DO UPDATE SET " + "class_name=CASE WHEN excluded.class_name<>'' THEN excluded.class_name ELSE classes.class_name END," + "updated_at=excluded.updated_at"; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false; + sqliteBindText(stmt, 1, normalized_id); + sqliteBindText(stmt, 2, normalized_name); + sqliteBindText(stmt, 3, now); + bool ok = sqlite3_step(stmt) == SQLITE_DONE; + sqlite3_finalize(stmt); + return ok; +} + +bool ensureClassExists(sqlite3* db, const std::string& class_id, const std::string& now) { + if (!db) return false; + const std::string normalized_id = normalizedClassId(class_id); + sqlite3_stmt* stmt = nullptr; + const char* sql = "INSERT OR IGNORE INTO classes(class_id,class_name,updated_at) VALUES(?,?,?)"; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false; + sqliteBindText(stmt, 1, normalized_id); + sqliteBindText(stmt, 2, defaultClassName(normalized_id)); + sqliteBindText(stmt, 3, now); + bool ok = sqlite3_step(stmt) == SQLITE_DONE; + sqlite3_finalize(stmt); + return ok; +} + +bool upsertStudent(sqlite3* db, const std::string& student_id, const std::string& student_name, + const std::string& class_id, const std::string& now) { + if (!db || student_id.empty()) return false; + const std::string insert_class_id = normalizedClassId(class_id); + if (!class_id.empty() && !ensureClassExists(db, insert_class_id, now)) { + return false; + } + + sqlite3_stmt* stmt = nullptr; + const char* sql = + "INSERT INTO students(student_id,student_name,class_id,status,created_at,updated_at) " + "VALUES(?,?,?,?,?,?) " + "ON CONFLICT(student_id) DO UPDATE SET " + "student_name=CASE WHEN excluded.student_name<>'' THEN excluded.student_name ELSE students.student_name END," + "class_id=CASE WHEN ?<>'' THEN excluded.class_id ELSE students.class_id END," + "status='active'," + "updated_at=excluded.updated_at"; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false; + sqliteBindText(stmt, 1, student_id); + sqliteBindText(stmt, 2, student_name); + sqliteBindText(stmt, 3, insert_class_id); + sqliteBindText(stmt, 4, "active"); + sqliteBindText(stmt, 5, now); + sqliteBindText(stmt, 6, now); + sqliteBindText(stmt, 7, class_id); + bool ok = sqlite3_step(stmt) == SQLITE_DONE; + sqlite3_finalize(stmt); + return ok; +} + +bool upsertFaceProfile(sqlite3* db, const std::string& face_id, const std::string& student_id, + const std::string& status, const std::string& now) { + if (!db || face_id.empty()) return false; + sqlite3_stmt* stmt = nullptr; + const char* sql = + "INSERT INTO face_profiles(face_id,student_id,device_id,status,created_at,updated_at) " + "VALUES(?,?,?,?,?,?) " + "ON CONFLICT(face_id) DO UPDATE SET " + "student_id=CASE WHEN excluded.student_id<>'' THEN excluded.student_id ELSE face_profiles.student_id END," + "device_id=excluded.device_id," + "status=excluded.status," + "updated_at=excluded.updated_at"; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false; + sqliteBindText(stmt, 1, face_id); + sqliteBindText(stmt, 2, student_id); + sqliteBindText(stmt, 3, "k210-01"); + sqliteBindText(stmt, 4, status.empty() ? "active" : status); + sqliteBindText(stmt, 5, now); + sqliteBindText(stmt, 6, now); + bool ok = sqlite3_step(stmt) == SQLITE_DONE; + sqlite3_finalize(stmt); + return ok; +} + +bool clearFaceProfileBinding(sqlite3* db, const std::string& face_id, const std::string& now) { + if (!db || face_id.empty()) return false; + sqlite3_stmt* stmt = nullptr; + bool ok = false; + const char* sql = + "UPDATE face_profiles SET " + "student_id=''," + "status=CASE WHEN EXISTS(SELECT 1 FROM face_photos WHERE face_photos.face_id=face_profiles.face_id) " + "THEN 'active' ELSE 'inactive' END," + "updated_at=? " + "WHERE face_id=?"; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) { + sqliteBindText(stmt, 1, now); + sqliteBindText(stmt, 2, face_id); + ok = sqlite3_step(stmt) == SQLITE_DONE; + } + sqlite3_finalize(stmt); + return ok; +} + +sqlite3_int64 getFileSize(const std::string& filepath) { + std::ifstream file(filepath.c_str(), std::ios::binary | std::ios::ate); + if (!file.is_open()) return 0; + return static_cast(file.tellg()); +} + +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() { + sqlite3* db = openSqliteDb(); + if (!db) return 0; + int count = sqliteCountRows(db, "face_photos"); + sqlite3_close(db); + 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); + if (!ok) return false; + + upsertStudent(db, student_id, name, class_id, timestamp); + + return true; +} + +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; +} + +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 classes(" + "class_id TEXT PRIMARY KEY," + "class_name TEXT NOT NULL," + "updated_at TEXT NOT NULL" + ");" + "CREATE TABLE IF NOT EXISTS students(" + "student_id TEXT PRIMARY KEY," + "student_name TEXT," + "class_id TEXT DEFAULT '0'," + "status TEXT NOT NULL DEFAULT 'active'," + "created_at TEXT NOT NULL," + "updated_at TEXT NOT NULL" + ");" + "CREATE INDEX IF NOT EXISTS idx_students_class ON students(class_id);" + "CREATE TABLE IF NOT EXISTS face_profiles(" + "face_id TEXT PRIMARY KEY," + "student_id TEXT," + "device_id TEXT DEFAULT 'k210-01'," + "status TEXT NOT NULL DEFAULT 'active'," + "created_at TEXT NOT NULL," + "updated_at TEXT NOT NULL" + ");" + "CREATE INDEX IF NOT EXISTS idx_face_profiles_student ON face_profiles(student_id);" + "CREATE TABLE IF NOT EXISTS face_photos(" + "face_id TEXT PRIMARY KEY," + "file_path TEXT NOT NULL," + "content_type TEXT DEFAULT 'image/jpeg'," + "file_size INTEGER DEFAULT 0," + "uploaded_at 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='migrated-' || 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 (sqliteGetKv(db, "time_slots", "").empty()) { + sqliteSetKv(db, "time_slots", DEFAULT_TIMESLOTS_JSON); + } + if (sqliteGetKv(db, "mapping_config", "").empty()) { + sqliteSetKv(db, "mapping_config", "{\"student_name_map\":{},\"student_class_map\":{},\"class_name_map\":{}}"); + } + if (sqliteGetKv(db, "sync_request", "").empty()) { + sqliteSetKv(db, "sync_request", "{\"sync\":false}"); + } + if (sqliteGetKv(db, "sync_version", "").empty()) { + sqliteSetKv(db, "sync_version", "1"); + } + + const std::string now = getCurrentTimestamp(); + upsertClass(db, "0", "默认班级", now); + + sqlite3_stmt* migration_stmt = nullptr; + if (sqlite3_prepare_v2(db, "SELECT face_id,student_id FROM face_mappings", -1, &migration_stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(migration_stmt) == SQLITE_ROW) { + upsertFaceProfile(db, sqliteColumnText(migration_stmt, 0), sqliteColumnText(migration_stmt, 1), "active", now); + } + } + sqlite3_finalize(migration_stmt); + + if (!sqliteTableHasRows(db, "students")) { + const std::string mapping_config = sqliteGetKv(db, "mapping_config", "{\"student_name_map\":{},\"student_class_map\":{},\"class_name_map\":{}}"); + const std::map student_names = parseSimpleStringMap(mapping_config, "student_name_map"); + const std::map student_classes = parseSimpleStringMap(mapping_config, "student_class_map"); + const std::map class_names = parseSimpleStringMap(mapping_config, "class_name_map"); + + for (const auto& item : class_names) { + upsertClass(db, item.first, item.second, now); + } + + for (const auto& item : student_names) { + const auto class_it = student_classes.find(item.first); + upsertStudent(db, item.first, item.second, + class_it == student_classes.end() ? "" : class_it->second, now); + } + + for (const auto& item : student_classes) { + if (!student_names.count(item.first)) { + upsertStudent(db, item.first, "", item.second, now); + } + } + + if (sqlite3_prepare_v2(db, "SELECT DISTINCT student_id,name,COALESCE(NULLIF(class_id,''),'0') FROM records WHERE student_id<>''", + -1, &migration_stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(migration_stmt) == SQLITE_ROW) { + upsertStudent(db, sqliteColumnText(migration_stmt, 0), sqliteColumnText(migration_stmt, 1), + sqliteColumnText(migration_stmt, 2), now); + } + } + sqlite3_finalize(migration_stmt); + + migration_stmt = nullptr; + if (sqlite3_prepare_v2(db, "SELECT DISTINCT student_id FROM face_mappings WHERE student_id<>''", + -1, &migration_stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(migration_stmt) == SQLITE_ROW) { + upsertStudent(db, sqliteColumnText(migration_stmt, 0), "", "", now); + } + } + sqlite3_finalize(migration_stmt); + } + + _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; +} + +std::map parseSimpleStringMap(const std::string& json_data, const std::string& key) { + std::map values; + std::string object = extractJsonMemberValue(json_data, key, "{}"); + if (object.size() < 2) return values; + + size_t pos = 1; + while (pos < object.size()) { + while (pos < object.size() && (std::isspace(static_cast(object[pos])) || object[pos] == ',')) pos++; + if (pos >= object.size() || object[pos] == '}') break; + + std::string item_key; + if (!parseJsonStringLiteral(object, pos, item_key)) break; + while (pos < object.size() && std::isspace(static_cast(object[pos]))) pos++; + if (pos >= object.size() || object[pos] != ':') break; + pos++; + while (pos < object.size() && std::isspace(static_cast(object[pos]))) pos++; + + std::string item_value; + if (pos < object.size() && object[pos] == '"') { + if (!parseJsonStringLiteral(object, pos, item_value)) break; + } else { + size_t start = pos; + while (pos < object.size() && object[pos] != ',' && object[pos] != '}') pos++; + item_value = object.substr(start, pos - start); + size_t first = item_value.find_first_not_of(" \t\r\n"); + size_t last = item_value.find_last_not_of(" \t\r\n"); + item_value = first == std::string::npos ? "" : item_value.substr(first, last - first + 1); + } + + if (!item_key.empty() && !item_value.empty()) { + values[item_key] = item_value; + } + } + return values; +} + +bool saveMappingConfig(const std::string& json_data) { + if (json_data.empty()) return false; + if (extractJsonMemberValue(json_data, "student_name_map", "").empty() + || extractJsonMemberValue(json_data, "student_class_map", "").empty() + || extractJsonMemberValue(json_data, "class_name_map", "").empty()) { + return false; + } + const std::map student_names = parseSimpleStringMap(json_data, "student_name_map"); + const std::map student_classes = parseSimpleStringMap(json_data, "student_class_map"); + const std::map class_names = parseSimpleStringMap(json_data, "class_name_map"); + + sqlite3* db = openSqliteDb(); + if (!db) return false; + + const std::string now = getCurrentTimestamp(); + bool ok = sqliteExec(db, "BEGIN") + && sqliteSetKv(db, "mapping_config", json_data); + + if (ok) { + for (const auto& item : class_names) { + if (!upsertClass(db, item.first, item.second, now)) { + ok = false; + break; + } + } + } + + if (ok) { + for (const auto& item : student_names) { + const auto class_it = student_classes.find(item.first); + if (!upsertStudent(db, item.first, item.second, + class_it == student_classes.end() ? "" : class_it->second, now)) { + ok = false; + break; + } + } + } + + if (ok) { + for (const auto& item : student_classes) { + if (student_names.count(item.first)) continue; + if (!upsertStudent(db, item.first, "", item.second, now)) { + ok = false; + break; + } + } + } + + if (ok) { + sqlite3_stmt* clear_stmt = nullptr; + if (sqlite3_prepare_v2(db, + "UPDATE students SET student_name='',updated_at=? WHERE COALESCE(student_name,'')<>'' AND student_id=?", + -1, &clear_stmt, nullptr) == SQLITE_OK) { + sqlite3_stmt* select_stmt = nullptr; + std::vector existing_ids; + if (sqlite3_prepare_v2(db, + "SELECT student_id FROM students WHERE COALESCE(student_name,'')<>''", + -1, &select_stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(select_stmt) == SQLITE_ROW) { + existing_ids.push_back(sqliteColumnText(select_stmt, 0)); + } + } + sqlite3_finalize(select_stmt); + + for (const std::string& student_id : existing_ids) { + if (student_names.count(student_id)) continue; + sqlite3_reset(clear_stmt); + sqlite3_clear_bindings(clear_stmt); + sqliteBindText(clear_stmt, 1, now); + sqliteBindText(clear_stmt, 2, student_id); + if (sqlite3_step(clear_stmt) != SQLITE_DONE) { + ok = false; + break; + } + } + } else { + ok = false; + } + sqlite3_finalize(clear_stmt); + } + + if (ok) { + sqlite3_stmt* clear_stmt = nullptr; + if (sqlite3_prepare_v2(db, + "UPDATE students SET class_id='0',updated_at=? WHERE COALESCE(class_id,'0')<>'0' AND student_id=?", + -1, &clear_stmt, nullptr) == SQLITE_OK) { + sqlite3_stmt* select_stmt = nullptr; + std::vector existing_ids; + if (sqlite3_prepare_v2(db, + "SELECT student_id FROM students WHERE COALESCE(class_id,'0')<>'0'", + -1, &select_stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(select_stmt) == SQLITE_ROW) { + existing_ids.push_back(sqliteColumnText(select_stmt, 0)); + } + } + sqlite3_finalize(select_stmt); + + for (const std::string& student_id : existing_ids) { + if (student_classes.count(student_id)) continue; + sqlite3_reset(clear_stmt); + sqlite3_clear_bindings(clear_stmt); + sqliteBindText(clear_stmt, 1, now); + sqliteBindText(clear_stmt, 2, student_id); + if (sqlite3_step(clear_stmt) != SQLITE_DONE) { + ok = false; + break; + } + } + } else { + ok = false; + } + sqlite3_finalize(clear_stmt); + } + + if (ok) { + sqlite3_stmt* clear_stmt = nullptr; + if (sqlite3_prepare_v2(db, + "UPDATE classes SET class_name='',updated_at=? WHERE class_id<>'0' AND COALESCE(class_name,'')<>'' AND class_id=?", + -1, &clear_stmt, nullptr) == SQLITE_OK) { + sqlite3_stmt* select_stmt = nullptr; + std::vector existing_ids; + if (sqlite3_prepare_v2(db, + "SELECT class_id FROM classes WHERE class_id<>'0' AND COALESCE(class_name,'')<>''", + -1, &select_stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(select_stmt) == SQLITE_ROW) { + existing_ids.push_back(sqliteColumnText(select_stmt, 0)); + } + } + sqlite3_finalize(select_stmt); + + for (const std::string& class_id : existing_ids) { + if (class_names.count(class_id)) continue; + sqlite3_reset(clear_stmt); + sqlite3_clear_bindings(clear_stmt); + sqliteBindText(clear_stmt, 1, now); + sqliteBindText(clear_stmt, 2, class_id); + if (sqlite3_step(clear_stmt) != SQLITE_DONE) { + ok = false; + break; + } + } + } else { + ok = false; + } + sqlite3_finalize(clear_stmt); + } + + sqliteExec(db, ok ? "COMMIT" : "ROLLBACK"); + sqlite3_close(db); + return ok; +} + +std::string readMappingConfig() { + sqlite3* db = openSqliteDb(); + if (!db) return sqliteGetKvValue("mapping_config", "{\"student_name_map\":{},\"student_class_map\":{},\"class_name_map\":{}}"); + + std::ostringstream json; + json << "{\"student_name_map\":{"; + + sqlite3_stmt* stmt = nullptr; + bool first = true; + if (sqlite3_prepare_v2(db, + "SELECT student_id,student_name FROM students WHERE COALESCE(student_name,'')<>'' ORDER BY student_id", + -1, &stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(stmt) == SQLITE_ROW) { + if (!first) json << ","; + json << "\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\":" + << "\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\""; + first = false; + } + } + sqlite3_finalize(stmt); + + json << "},\"student_class_map\":{"; + stmt = nullptr; + first = true; + if (sqlite3_prepare_v2(db, + "SELECT student_id,class_id FROM students WHERE COALESCE(class_id,'0')<>'0' ORDER BY student_id", + -1, &stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(stmt) == SQLITE_ROW) { + if (!first) json << ","; + json << "\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\":" + << "\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\""; + first = false; + } + } + sqlite3_finalize(stmt); + + json << "},\"class_name_map\":{"; + stmt = nullptr; + first = true; + if (sqlite3_prepare_v2(db, + "SELECT class_id,class_name FROM classes WHERE COALESCE(class_name,'')<>'' ORDER BY class_id", + -1, &stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(stmt) == SQLITE_ROW) { + if (!first) json << ","; + json << "\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\":" + << "\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\""; + first = false; + } + } + sqlite3_finalize(stmt); + sqlite3_close(db); + + json << "}}"; + return json.str(); +} + +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; + if (extractJsonMemberValue(json_data, "mappings", "").empty()) { + return false; + } + std::map mappings = parseFaceIdMappings(json_data); + sqlite3* db = openSqliteDb(); + if (!db) return false; + const std::string now = getCurrentTimestamp(); + std::set previously_bound_faces; + sqlite3_stmt* previous_stmt = nullptr; + if (sqlite3_prepare_v2(db, "SELECT face_id FROM face_mappings", -1, &previous_stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(previous_stmt) == SQLITE_ROW) { + previously_bound_faces.insert(sqliteColumnText(previous_stmt, 0)); + } + } + sqlite3_finalize(previous_stmt); + + 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) { + if (!isSafeId(m.first) || !isSafeId(m.second)) { + ok = false; + break; + } + 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); + + if (ok) { + for (const auto& m : mappings) { + previously_bound_faces.erase(m.first); + if (!upsertFaceProfile(db, m.first, m.second, "active", now)) { + ok = false; + break; + } + } + } + + if (ok) { + for (const auto& m : mappings) { + if (!upsertStudent(db, m.second, "", "", now)) { + ok = false; + break; + } + } + } + + if (ok) { + for (const std::string& face_id : previously_bound_faces) { + if (!clearFaceProfileBinding(db, face_id, now)) { + ok = false; + break; + } + } + } + + sqliteExec(db, ok ? "COMMIT" : "ROLLBACK"); + sqlite3_close(db); + return ok; +} + +std::map parseFaceIdMappings(const std::string& json_data) { + return parseSimpleStringMap(json_data, "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; +} + +// 读取人脸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\":["; + + sqlite3* db = openSqliteDb(); + bool first = true; + if (db) { + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, + "SELECT face_id,file_path FROM face_photos ORDER BY face_id", + -1, &stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(stmt) == SQLITE_ROW) { + const std::string face_id = sqliteColumnText(stmt, 0); + const std::string file_path = sqliteColumnText(stmt, 1); + if (!isSafeId(face_id) || getFileSize(file_path) <= 0) { + continue; + } + if (!first) json << ","; + json << "{\"id\":\"" << jsonEscape(face_id) << "\",\"filename\":\"" << jsonEscape(face_id + ".jpg") << "\"}"; + first = false; + } + } + sqlite3_finalize(stmt); + sqlite3_close(db); + } + + 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(); + sqlite3* db = openSqliteDb(); + if (db) { + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, + "REPLACE INTO face_photos(face_id,file_path,content_type,file_size,uploaded_at) VALUES(?,?,?,?,?)", + -1, &stmt, nullptr) == SQLITE_OK) { + sqliteBindText(stmt, 1, face_id); + sqliteBindText(stmt, 2, filepath); + sqliteBindText(stmt, 3, "image/jpeg"); + sqlite3_bind_int64(stmt, 4, static_cast(photo_data.size())); + sqliteBindText(stmt, 5, getCurrentTimestamp()); + sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + + upsertFaceProfile(db, face_id, "", "active", getCurrentTimestamp()); + sqlite3_close(db); + } + 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; + sqlite3* db = openSqliteDb(); + if (db) { + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, "DELETE FROM face_photos WHERE face_id=?", -1, &stmt, nullptr) == SQLITE_OK) { + sqliteBindText(stmt, 1, face_id); + sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + + clearFaceProfileBinding(db, face_id, getCurrentTimestamp()); + sqlite3_close(db); + } + 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); + clearFaceProfileBinding(db, face_id, getCurrentTimestamp()); + 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 student_id FROM students WHERE student_id<>'' " + "UNION SELECT student_id FROM records WHERE student_id<>'' " + "UNION SELECT student_id FROM face_mappings 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, Authorization, 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); + } + + if (method != "OPTIONS" && endpoint.find("/api/v1/") != 0) { + response_body = "{\"error\":\"Not Found\"}"; + status_code = 404; + status_text = "Not Found"; + sendResponse(); + return; + } + + 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/v1/faces/photos/") == 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/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/v1/sync/ack") { + response_body = makeSyncAckResponse(request_body); + std::cout << "[SYNC] Ack received: " << request_body << std::endl; + + // 新增:处理加密签到数据 + } else if (method == "POST" && endpoint == "/api/v1/attendance/checkins") { + 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/v1/attendance/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/v1/attendance/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/v1/attendance/records/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/v1/attendance/records") { + 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/v1/system/health") { + response_body = makeHealthResponse(); + + } else if (method == "GET" && endpoint == "/api/v1/system/storage") { + response_body = makeStorageStatusResponse(); + + } else if (method == "GET" && endpoint == "/api/v1/system/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/v1/config/time-slots") { + // 上传时间段配置: POST /api/v1/config/time-slots + 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/v1/config/time-slots") { + // 获取时间段配置: GET /api/v1/config/time-slots + std::cout << "[TIMESLOTS] Reading configuration" << std::endl; + response_body = readTimeSlots(); + + } else if (method == "GET" && endpoint == "/api/v1/attendance/duplicates") { + // 检查重复签到: GET /api/v1/attendance/duplicates?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/v1/attendance/today") { + // 获取今日已签到记录(供STM32开机下载) + // GET /api/v1/attendance/today?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/v1/config/mappings") { + // 上传映射配置: POST /api/v1/config/mappings + 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/v1/config/mappings") { + // 获取映射配置: GET /api/v1/config/mappings + 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/v1/faces/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/v1/faces/photos/") == 0) { + // 获取指定人脸照片: GET /api/v1/faces/photos/ + std::string face_id = urlDecode(endpoint.substr(std::string("/api/v1/faces/photos/").size())); + 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/v1/faces/photos/") == 0) { + // 人脸照片上传: POST /api/v1/faces/photos/ + std::string face_id = urlDecode(endpoint.substr(std::string("/api/v1/faces/photos/").size())); + 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/v1/faces/mappings") { + // 保存人脸ID映射 + std::cout << "[FACE_MAPPING] Saving mapping, size: " << request_body.size() << " bytes" << std::endl; + if (saveFaceIdMapping(request_body)) { + 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/v1/faces/mappings") { + // 获取人脸ID映射 + response_body = readFaceIdMapping(); + std::cout << "[FACE_MAPPING] Mapping fetched" << std::endl; + + } else if (method == "GET" && endpoint == "/api/v1/faces/student-ids") { + // 获取所有学生ID(从考勤记录提取) + response_body = getAllStudentIds(); + std::cout << "[STUDENT_IDS] All student IDs fetched" << std::endl; + + } else if (method == "DELETE" && endpoint.find("/api/v1/faces/") == 0) { + // 删除人脸信息: DELETE /api/v1/faces/?delete_records=true + std::string student_id = urlDecode(endpoint.substr(std::string("/api/v1/faces/").size())); + 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/v1/faces/pending-delete") { + // K210查询待删除列表 + response_body = getPendingDeleteList(); + std::cout << "[PENDING_DELETE] List fetched" << std::endl; + + } else if (method == "POST" && endpoint == "/api/v1/faces/delete-ack") { + // 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/v1/sync/request") { + // K210查询是否需要同步 + response_body = getSyncRequest(); + std::cout << "[SYNC] K210 checking sync request: " << response_body << std::endl; + + } else if (method == "POST" && endpoint == "/api/v1/sync/request/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/v1/sync/trigger") { + // 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,所有运行时数据统一写入 attendance.db + 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; +}