From e3abe2daa15626293fbbad489414552053e3174e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BA=AA=E7=94=B0=E9=87=8E?= Date: Mon, 4 May 2026 21:17:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=90=8C=E6=AD=A5=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- regression_api_tests.ps1 | 7 +- server_encrypted.cpp | 1415 +++++++++++++++++++------------------- 2 files changed, 726 insertions(+), 696 deletions(-) diff --git a/regression_api_tests.ps1 b/regression_api_tests.ps1 index 3d95abc..e602a83 100644 --- a/regression_api_tests.ps1 +++ b/regression_api_tests.ps1 @@ -52,11 +52,10 @@ 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 "students" "/api/v1/students" | Out-Null +Invoke-ApiCheck "classes" "/api/v1/classes" | Out-Null +Invoke-ApiCheck "face profiles" "/api/v1/face_profiles" | 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 index ac9962f..82a713d 100644 --- a/server_encrypted.cpp +++ b/server_encrypted.cpp @@ -58,6 +58,8 @@ 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 size_t MAX_ENCRYPTED_JSON_BODY_SIZE = ((MAX_JSON_BODY_SIZE + 16) * 4 / 3) + 32; +const size_t MAX_ENCRYPTED_PHOTO_BODY_SIZE = ((MAX_PHOTO_BODY_SIZE + 16) * 4 / 3) + 32; const char* DEFAULT_API_TOKEN = "atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV"; const char* DEFAULT_CORS_ORIGIN = "http://localhost"; const char* DEFAULT_TIMESLOTS_JSON = @@ -86,22 +88,25 @@ const std::string& bindAddress() { return address; } -std::string readFaceIdMapping(); +std::string readStudents(); +std::string readClasses(); +std::string readFaceProfiles(); std::string getPendingDeleteList(); std::string getFacePhotosList(); -std::string getAllStudentIds(); bool setSyncRequest(bool sync); bool removeFromPendingDelete(const std::string& face_id); +bool cancelPendingDeleteForFace(sqlite3* db, const std::string& face_id, const std::string& now); 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); +bool saveStudents(const std::string& json_data); +bool saveClasses(const std::string& json_data); +bool saveFaceProfiles(const std::string& json_data); +unsigned long long bumpSyncVersion(const std::string& reason); +std::vector> parseJsonObjectArray(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) { @@ -209,6 +214,11 @@ bool buildFacePhotoPath(const std::string& face_id, std::string& filepath) { return true; } +bool fileExistsAndNotEmpty(const std::string& filepath) { + std::ifstream file(filepath.c_str(), std::ios::binary | std::ios::ate); + return file.is_open() && file.tellg() > 0; +} + 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); @@ -321,7 +331,8 @@ bool hasBearerToken(const std::string& request, const std::string& expected) { } bool isPublicEndpoint(const std::string& method, const std::string& endpoint) { - return method == "OPTIONS" || (method == "GET" && endpoint == "/api/v1/system/health"); + (void)endpoint; + return method == "OPTIONS"; } bool isAuthorizedRequest(const std::string& request, const std::string& method, const std::string& endpoint) { @@ -335,6 +346,19 @@ std::string makeJsonError(const std::string& message) { return "{\"success\":false,\"message\":\"" + jsonEscape(message) + "\"}"; } +bool isEncryptedRequest(const std::string& request) { + return hasHeaderToken(request, "X-Encrypted", "AES128-CBC"); +} + +bool isJsonLikeContentType(const std::string& content_type) { + std::string lower = content_type; + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return lower.find("application/json") != std::string::npos + || lower.find("text/json") != std::string::npos + || lower.find("+json") != std::string::npos; +} + // ⚠️ 重要:密钥和IV必须与STM32端完全一致! static const unsigned char MY_AES_KEY[16] = { 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, @@ -349,7 +373,10 @@ static const unsigned char MY_AES_IV[16] = { // ============= Base64解码 ============= std::string base64_decode(const std::string& encoded_string) { BIO *bio, *b64; - int decode_len = encoded_string.length(); + int decode_len = static_cast(encoded_string.length()); + if (decode_len <= 0) { + return ""; + } char* buffer = new char[decode_len]; bio = BIO_new_mem_buf(encoded_string.c_str(), -1); @@ -359,6 +386,10 @@ std::string base64_decode(const std::string& encoded_string) { BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); int length = BIO_read(bio, buffer, decode_len); BIO_free_all(bio); + if (length <= 0) { + delete[] buffer; + return ""; + } std::string result(buffer, length); delete[] buffer; @@ -534,12 +565,12 @@ std::string base64_encode(const std::string& input) { return result; } -// ============= 加密JSON数据(用于响应Qt客户端)============= -std::string encryptJSON(const std::string& json_data) { - std::cout << "[CRYPTO] Encrypting response..." << std::endl; +// ============= 加密传输数据 ============= +std::string encryptPayload(const std::string& plain_data) { + std::cout << "[CRYPTO] Encrypting payload (" << plain_data.size() << " bytes)..." << std::endl; // AES加密 - std::string encrypted = aes_encrypt_cbc(json_data); + std::string encrypted = aes_encrypt_cbc(plain_data); if (encrypted.empty()) { std::cerr << "[CRYPTO] Encryption failed!" << std::endl; return ""; @@ -548,15 +579,18 @@ std::string encryptJSON(const std::string& json_data) { // Base64编码 std::string encrypted_base64 = base64_encode(encrypted); - std::cout << "[CRYPTO] ✓ Response encrypted" << std::endl; + std::cout << "[CRYPTO] ✓ Payload encrypted" << std::endl; return encrypted_base64; } -// ============= 解密JSON数据 ============= -std::string decryptJSON(const std::string& encrypted_base64) { +std::string encryptJSON(const std::string& json_data) { + return encryptPayload(json_data); +} + +// ============= 解密传输数据 ============= +std::string decryptPayload(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] Decrypting payload..." << std::endl; std::cout << "[CRYPTO] Base64 length: " << encrypted_base64.length() << std::endl; // 1. Base64解码 @@ -579,7 +613,6 @@ std::string decryptJSON(const std::string& encrypted_base64) { 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; @@ -589,6 +622,10 @@ std::string decryptJSON(const std::string& encrypted_base64) { } } +std::string decryptJSON(const std::string& encrypted_base64) { + return decryptPayload(encrypted_base64); +} + // ============= 工具函数 ============= std::string getCurrentTimestamp() { time_t now = time(0); @@ -773,33 +810,6 @@ 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; @@ -1007,6 +1017,122 @@ sqlite3_int64 getFileSize(const std::string& filepath) { return static_cast(file.tellg()); } +bool facePhotoExists(sqlite3* db, const std::string& face_id) { + if (!db || !isSafeId(face_id)) return false; + + sqlite3_stmt* stmt = nullptr; + bool exists = false; + if (sqlite3_prepare_v2(db, + "SELECT file_path FROM face_photos WHERE face_id=?", + -1, &stmt, nullptr) == SQLITE_OK) { + sqliteBindText(stmt, 1, face_id); + if (sqlite3_step(stmt) == SQLITE_ROW) { + exists = getFileSize(sqliteColumnText(stmt, 0)) > 0; + } + } + sqlite3_finalize(stmt); + + if (!exists) { + std::string filepath; + exists = buildFacePhotoPath(face_id, filepath) && fileExistsAndNotEmpty(filepath); + } + return exists; +} + +bool faceProfileExists(sqlite3* db, const std::string& face_id) { + if (!db || !isSafeId(face_id)) return false; + sqlite3_stmt* stmt = nullptr; + bool exists = false; + if (sqlite3_prepare_v2(db, + "SELECT 1 FROM face_profiles WHERE face_id=? LIMIT 1", + -1, &stmt, nullptr) == SQLITE_OK) { + sqliteBindText(stmt, 1, face_id); + exists = sqlite3_step(stmt) == SQLITE_ROW; + } + sqlite3_finalize(stmt); + return exists; +} + +std::vector unboundPhotoCandidates(sqlite3* db, + const std::string& wrong_face_id, + const std::string& student_id) { + std::vector candidates; + if (!db || student_id.empty()) return candidates; + + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, + "SELECT ph.face_id FROM face_photos ph " + "LEFT JOIN face_profiles fp ON fp.face_id=ph.face_id " + "WHERE ph.face_id<>? AND (COALESCE(fp.student_id,'')='' OR fp.student_id=?) " + "ORDER BY ph.face_id", + -1, &stmt, nullptr) == SQLITE_OK) { + sqliteBindText(stmt, 1, wrong_face_id); + sqliteBindText(stmt, 2, student_id); + while (sqlite3_step(stmt) == SQLITE_ROW) { + const std::string candidate = sqliteColumnText(stmt, 0); + if (isSafeId(candidate)) { + candidates.push_back(candidate); + } + } + } + sqlite3_finalize(stmt); + return candidates; +} + +bool deleteFaceProfileOnly(sqlite3* db, const std::string& face_id) { + if (!db || !isSafeId(face_id)) return false; + sqlite3_stmt* stmt = nullptr; + bool ok = false; + if (sqlite3_prepare_v2(db, + "DELETE FROM face_profiles WHERE face_id=?", + -1, &stmt, nullptr) == SQLITE_OK) { + sqliteBindText(stmt, 1, face_id); + ok = sqlite3_step(stmt) == SQLITE_DONE; + } + sqlite3_finalize(stmt); + return ok; +} + +int reconcileFaceProfilesWithPhotos(sqlite3* db, const std::string& now) { + if (!db) return 0; + + struct Row { + std::string face_id; + std::string student_id; + }; + std::vector rows; + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, + "SELECT fp.face_id,COALESCE(fp.student_id,'') " + "FROM face_profiles fp " + "LEFT JOIN face_photos ph ON ph.face_id=fp.face_id " + "WHERE COALESCE(fp.student_id,'')<>'' " + "AND (ph.face_id IS NULL OR fp.face_id=fp.student_id)", + -1, &stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(stmt) == SQLITE_ROW) { + rows.push_back({sqliteColumnText(stmt, 0), sqliteColumnText(stmt, 1)}); + } + } + sqlite3_finalize(stmt); + + int fixed = 0; + for (const Row& row : rows) { + const std::vector candidates = unboundPhotoCandidates(db, row.face_id, row.student_id); + if (candidates.size() != 1) { + continue; + } + const std::string& real_face_id = candidates.front(); + if (upsertFaceProfile(db, real_face_id, row.student_id, "active", now) + && deleteFaceProfileOnly(db, row.face_id)) { + fixed++; + std::cout << "[FACE_PROFILE] Repaired mapping: wrong face_id " + << row.face_id << " -> real face_id " << real_face_id + << " for student " << row.student_id << std::endl; + } + } + return fixed; +} + bool sqliteQuickCheck(std::string& message) { sqlite3* db = openSqliteDb(); if (!db) { @@ -1088,10 +1214,6 @@ bool initializeSqliteStorage() { "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," @@ -1143,31 +1265,9 @@ bool initializeSqliteStorage() { 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}"); } @@ -1178,55 +1278,6 @@ bool initializeSqliteStorage() { 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; @@ -1339,63 +1390,120 @@ std::string readTimeSlots() { 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; +std::string readStudents() { + std::ostringstream json; + json << "{\"students\":["; + sqlite3* db = openSqliteDb(); + bool first = true; + if (db) { + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, + "SELECT student_id,COALESCE(student_name,''),COALESCE(class_id,'0'),COALESCE(status,'active') " + "FROM students WHERE student_id<>'' AND COALESCE(status,'active')<>'deleted' ORDER BY student_id", + -1, &stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(stmt) == SQLITE_ROW) { + if (!first) json << ","; + json << "{\"student_id\":\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\"," + << "\"student_name\":\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\"," + << "\"class_id\":\"" << jsonEscape(sqliteColumnText(stmt, 2)) << "\"," + << "\"status\":\"" << jsonEscape(sqliteColumnText(stmt, 3)) << "\"}"; + first = false; + } } + sqlite3_finalize(stmt); + sqlite3_close(db); } - return values; + json << "]}"; + return json.str(); } -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; +std::string readClasses() { + std::ostringstream json; + json << "{\"classes\":["; + sqlite3* db = openSqliteDb(); + bool first = true; + if (db) { + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, + "SELECT class_id,COALESCE(class_name,'') FROM classes WHERE class_id<>'' ORDER BY class_id", + -1, &stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(stmt) == SQLITE_ROW) { + if (!first) json << ","; + json << "{\"class_id\":\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\"," + << "\"class_name\":\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\"}"; + first = false; + } + } + sqlite3_finalize(stmt); + sqlite3_close(db); } - 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"); + json << "]}"; + return json.str(); +} +std::string readFaceProfiles() { + std::ostringstream json; + json << "{\"face_profiles\":["; + sqlite3* db = openSqliteDb(); + bool first = true; + int repaired = 0; + if (db) { + repaired = reconcileFaceProfilesWithPhotos(db, getCurrentTimestamp()); + if (repaired > 0) { + std::cout << "[FACE_PROFILE] Repaired " << repaired + << " profile/photo mismatches before listing" << std::endl; + } + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, + "SELECT fp.face_id,COALESCE(fp.student_id,''),COALESCE(fp.device_id,'k210-01'),COALESCE(fp.status,'active')," + "CASE WHEN ph.face_id IS NULL THEN 0 ELSE 1 END " + "FROM face_profiles fp " + "LEFT JOIN face_photos ph ON ph.face_id=fp.face_id " + "WHERE fp.face_id<>'' " + "UNION " + "SELECT ph.face_id,'','k210-01','photo_only',1 FROM face_photos ph " + "LEFT JOIN face_profiles fp ON fp.face_id=ph.face_id " + "WHERE ph.face_id<>'' AND fp.face_id IS NULL " + "ORDER BY face_id", + -1, &stmt, nullptr) == SQLITE_OK) { + while (sqlite3_step(stmt) == SQLITE_ROW) { + if (!first) json << ","; + json << "{\"face_id\":\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\"," + << "\"student_id\":\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\"," + << "\"device_id\":\"" << jsonEscape(sqliteColumnText(stmt, 2)) << "\"," + << "\"status\":\"" << jsonEscape(sqliteColumnText(stmt, 3)) << "\"," + << "\"has_photo\":" << (sqlite3_column_int(stmt, 4) ? "true" : "false") << "}"; + first = false; + } + } + sqlite3_finalize(stmt); + sqlite3_close(db); + } + if (repaired > 0) { + bumpSyncVersion("face_profile_repair"); + } + json << "]}"; + return json.str(); +} + +bool saveStudents(const std::string& json_data) { + const std::vector> students = parseJsonObjectArray(json_data, "students"); sqlite3* db = openSqliteDb(); if (!db) return false; - const std::string now = getCurrentTimestamp(); - bool ok = sqliteExec(db, "BEGIN") - && sqliteSetKv(db, "mapping_config", json_data); + bool ok = sqliteExec(db, "BEGIN"); if (ok) { - for (const auto& item : class_names) { - if (!upsertClass(db, item.first, item.second, now)) { + for (const auto& student : students) { + const auto id_it = student.find("student_id"); + const std::string student_id = id_it == student.end() ? "" : id_it->second; + if (!isSafeId(student_id)) { + ok = false; + break; + } + const std::string name = student.count("student_name") ? student.at("student_name") : ""; + const std::string class_id = student.count("class_id") ? normalizedClassId(student.at("class_id")) : "0"; + if (!isSafeId(class_id) || name.size() > 64 || !upsertStudent(db, student_id, name, class_id, now)) { ok = false; break; } @@ -1403,35 +1511,20 @@ bool saveMappingConfig(const std::string& json_data) { } 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; + sqlite3_stmt* 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; + "UPDATE students SET status='deleted',updated_at=? WHERE student_id=?", + -1, &stmt, nullptr) == SQLITE_OK) { + sqlite3_stmt* clear_face_stmt = nullptr; + const bool clear_face_ready = sqlite3_prepare_v2(db, + "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 'unbound' END,updated_at=? WHERE student_id=?", + -1, &clear_face_stmt, nullptr) == SQLITE_OK; std::vector existing_ids; + sqlite3_stmt* select_stmt = nullptr; if (sqlite3_prepare_v2(db, - "SELECT student_id FROM students WHERE COALESCE(student_name,'')<>''", + "SELECT student_id FROM students WHERE COALESCE(status,'active')<>'deleted'", -1, &select_stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(select_stmt) == SQLITE_ROW) { existing_ids.push_back(sqliteColumnText(select_stmt, 0)); @@ -1439,78 +1532,204 @@ bool saveMappingConfig(const std::string& json_data) { } sqlite3_finalize(select_stmt); + std::set incoming_ids; + for (const auto& student : students) { + auto it = student.find("student_id"); + if (it != student.end()) incoming_ids.insert(it->second); + } 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) { + if (student_id == "UNKNOWN" || incoming_ids.count(student_id)) continue; + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + sqliteBindText(stmt, 1, now); + sqliteBindText(stmt, 2, student_id); + if (sqlite3_step(stmt) != SQLITE_DONE) { + ok = false; + break; + } + if (clear_face_ready) { + sqlite3_reset(clear_face_stmt); + sqlite3_clear_bindings(clear_face_stmt); + sqliteBindText(clear_face_stmt, 1, now); + sqliteBindText(clear_face_stmt, 2, student_id); + if (sqlite3_step(clear_face_stmt) != SQLITE_DONE) { + ok = false; + break; + } + } + } + sqlite3_finalize(clear_face_stmt); + } else { + ok = false; + } + sqlite3_finalize(stmt); + } + + sqliteExec(db, ok ? "COMMIT" : "ROLLBACK"); + sqlite3_close(db); + return ok; +} + +bool saveClasses(const std::string& json_data) { + const std::vector> classes = parseJsonObjectArray(json_data, "classes"); + sqlite3* db = openSqliteDb(); + if (!db) return false; + const std::string now = getCurrentTimestamp(); + bool ok = sqliteExec(db, "BEGIN"); + std::set incoming_ids; + + if (ok) { + for (const auto& row : classes) { + const std::string class_id = row.count("class_id") ? normalizedClassId(row.at("class_id")) : ""; + const std::string class_name = row.count("class_name") ? row.at("class_name") : ""; + if (!isSafeId(class_id) || class_name.empty() || class_name.size() > 64) { + ok = false; + break; + } + incoming_ids.insert(class_id); + if (!upsertClass(db, class_id, class_name, now)) { + ok = false; + break; + } + } + } + + if (ok) { + sqlite3_stmt* select_stmt = nullptr; + sqlite3_stmt* count_stmt = nullptr; + sqlite3_stmt* delete_stmt = nullptr; + std::vector existing_ids; + if (sqlite3_prepare_v2(db, "SELECT class_id FROM classes WHERE class_id<>'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); + + if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM students WHERE class_id=? AND COALESCE(status,'active')<>'deleted'", + -1, &count_stmt, nullptr) != SQLITE_OK + || sqlite3_prepare_v2(db, "DELETE FROM classes WHERE class_id=?", -1, &delete_stmt, nullptr) != SQLITE_OK) { + ok = false; + } + + for (const std::string& class_id : existing_ids) { + if (!ok || incoming_ids.count(class_id)) continue; + sqlite3_reset(count_stmt); + sqlite3_clear_bindings(count_stmt); + sqliteBindText(count_stmt, 1, class_id); + int student_count = 0; + if (sqlite3_step(count_stmt) == SQLITE_ROW) { + student_count = sqlite3_column_int(count_stmt, 0); + } + if (student_count > 0) { + ok = false; + break; + } + sqlite3_reset(delete_stmt); + sqlite3_clear_bindings(delete_stmt); + sqliteBindText(delete_stmt, 1, class_id); + if (sqlite3_step(delete_stmt) != SQLITE_DONE) { + ok = false; + break; + } + } + sqlite3_finalize(count_stmt); + sqlite3_finalize(delete_stmt); + } + + sqliteExec(db, ok ? "COMMIT" : "ROLLBACK"); + sqlite3_close(db); + return ok; +} + +bool saveFaceProfiles(const std::string& json_data) { + const std::vector> profiles = parseJsonObjectArray(json_data, "face_profiles"); + sqlite3* db = openSqliteDb(); + if (!db) return false; + const std::string now = getCurrentTimestamp(); + bool ok = sqliteExec(db, "BEGIN"); + std::set incoming_face_ids; + std::set bound_face_ids; + + if (ok) { + for (const auto& profile : profiles) { + const std::string face_id = profile.count("face_id") ? profile.at("face_id") : ""; + const std::string student_id = profile.count("student_id") ? profile.at("student_id") : ""; + const std::string status = profile.count("status") ? profile.at("status") : "active"; + if (!isSafeId(face_id) || (!student_id.empty() && !isSafeId(student_id))) { + ok = false; + break; + } + + std::string effective_face_id = face_id; + if (!student_id.empty() && !facePhotoExists(db, effective_face_id)) { + const std::vector candidates = unboundPhotoCandidates(db, effective_face_id, student_id); + if (candidates.size() == 1) { + effective_face_id = candidates.front(); + deleteFaceProfileOnly(db, face_id); + std::cout << "[FACE_PROFILE] Corrected submitted face_id " << face_id + << " -> " << effective_face_id + << " for student " << student_id << std::endl; + } else { + std::cerr << "[FACE_PROFILE] Reject mapping without K210 photo: face_id=" + << face_id << ", student_id=" << student_id << std::endl; 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)); - } + if (student_id.empty() && bound_face_ids.count(effective_face_id)) { + incoming_face_ids.insert(effective_face_id); + continue; } - 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) { + if (!student_id.empty()) { + cancelPendingDeleteForFace(db, effective_face_id, now); + } + + incoming_face_ids.insert(effective_face_id); + if (!upsertFaceProfile(db, effective_face_id, student_id, status.empty() ? "active" : status, now)) { + ok = false; + break; + } + if (!student_id.empty()) { + bound_face_ids.insert(effective_face_id); + } + if (student_id.empty()) { + if (!clearFaceProfileBinding(db, effective_face_id, now)) { + ok = false; + break; + } + } + if (!student_id.empty()) { + if (!upsertStudent(db, student_id, "", "", now)) { 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=?", + "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 'unbound' END,updated_at=? WHERE face_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) { + if (sqlite3_prepare_v2(db, "SELECT face_id FROM face_profiles", -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; + for (const std::string& face_id : existing_ids) { + if (incoming_face_ids.count(face_id)) continue; sqlite3_reset(clear_stmt); sqlite3_clear_bindings(clear_stmt); sqliteBindText(clear_stmt, 1, now); - sqliteBindText(clear_stmt, 2, class_id); + sqliteBindText(clear_stmt, 2, face_id); if (sqlite3_step(clear_stmt) != SQLITE_DONE) { ok = false; break; @@ -1527,62 +1746,6 @@ bool saveMappingConfig(const std::string& json_data) { 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"); @@ -1692,6 +1855,53 @@ std::vector extractJsonStringArray(const std::string& json, const s return values; } +std::vector> parseJsonObjectArray(const std::string& json_data, const std::string& key) { + std::vector> rows; + const std::string array = extractJsonMemberValue(json_data, key, "[]"); + size_t pos = 0; + while (pos < array.size()) { + size_t start = array.find('{', pos); + if (start == std::string::npos) break; + size_t end = findMatchingJsonEnd(array, start); + if (end == std::string::npos) break; + + const std::string object = array.substr(start, end - start + 1); + std::map row; + size_t item_pos = 1; + while (item_pos < object.size()) { + while (item_pos < object.size() && std::isspace(static_cast(object[item_pos]))) item_pos++; + if (item_pos >= object.size() || object[item_pos] == '}') break; + + std::string item_key; + if (!parseJsonStringLiteral(object, item_pos, item_key)) break; + while (item_pos < object.size() && std::isspace(static_cast(object[item_pos]))) item_pos++; + if (item_pos >= object.size() || object[item_pos] != ':') break; + item_pos++; + while (item_pos < object.size() && std::isspace(static_cast(object[item_pos]))) item_pos++; + + std::string item_value; + if (item_pos < object.size() && object[item_pos] == '"') { + if (!parseJsonStringLiteral(object, item_pos, item_value)) break; + } else { + size_t value_start = item_pos; + while (item_pos < object.size() && object[item_pos] != ',' && object[item_pos] != '}') item_pos++; + item_value = object.substr(value_start, item_pos - value_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); + } + row[item_key] = item_value; + + while (item_pos < object.size() && std::isspace(static_cast(object[item_pos]))) item_pos++; + if (item_pos < object.size() && object[item_pos] == ',') item_pos++; + } + + rows.push_back(row); + pos = end + 1; + } + return rows; +} + 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); @@ -1722,16 +1932,18 @@ std::string makeSyncStateResponse(const std::string& client, unsigned long long if (changed) { json << ",\"state\":{"; bool first = true; + bool emitted_face_profiles = false; - if (client == "k210" || client == "qt" || client == "all" || client.empty()) { - const std::string mappings = extractJsonMemberValue(readFaceIdMapping(), "mappings", "{}"); + if (client == "k210" || client == "all" || client.empty()) { + const std::string profiles = extractJsonMemberValue(readFaceProfiles(), "face_profiles", "[]"); 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 + json << "\"face_profiles\":" << profiles << ",\"pending_delete\":" << pending << ",\"delete_tasks\":" << delete_tasks; first = false; + emitted_face_profiles = true; } if (client == "stm32" || client == "qt" || client == "all" || client.empty()) { @@ -1743,10 +1955,12 @@ std::string makeSyncStateResponse(const std::string& client, unsigned long long 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", "[]") + json << "\"students\":" << extractJsonMemberValue(readStudents(), "students", "[]") + << ",\"classes\":" << extractJsonMemberValue(readClasses(), "classes", "[]"); + if (!emitted_face_profiles) { + json << ",\"face_profiles\":" << extractJsonMemberValue(readFaceProfiles(), "face_profiles", "[]"); + } + json << ",\"face_photos\":" << extractJsonMemberValue(getFacePhotosList(), "photos", "[]") << ",\"records\":" << readDatabase(1000, 0) << ",\"statistics\":" << getStatistics(); } @@ -1806,12 +2020,12 @@ std::string makeStorageStatusResponse() { sqlite3* db = openSqliteDb(); int records = 0; - int face_mappings = 0; + int face_profiles = 0; int pending_deletes = 0; int face_photos = countFacePhotoFiles(); if (db) { records = sqliteCountRows(db, "records"); - face_mappings = sqliteCountRows(db, "face_mappings"); + face_profiles = sqliteCountRows(db, "face_profiles"); pending_deletes = sqliteCountRows(db, "pending_delete"); sqlite3_close(db); } @@ -1823,7 +2037,7 @@ std::string makeStorageStatusResponse() { << "\"ok\":" << (quick_ok ? "true" : "false") << "," << "\"quick_check\":\"" << jsonEscape(quick_check_message) << "\"," << "\"records\":" << records << "," - << "\"face_mappings\":" << face_mappings << "," + << "\"face_profiles\":" << face_profiles << "," << "\"face_photos\":" << face_photos << "," << "\"pending_deletes\":" << pending_deletes << "," << "\"sync_version\":" << readSyncVersion() << "," @@ -1907,147 +2121,6 @@ bool deleteRecord(const std::string& timestamp, const std::string& student_id) { // ============= 人脸照片管理函数 ============= -// 保存人脸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; @@ -2096,6 +2169,8 @@ bool saveFacePhoto(const std::string& face_id, const std::string& photo_data) { file.close(); sqlite3* db = openSqliteDb(); if (db) { + const std::string now = getCurrentTimestamp(); + cancelPendingDeleteForFace(db, face_id, now); sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "REPLACE INTO face_photos(face_id,file_path,content_type,file_size,uploaded_at) VALUES(?,?,?,?,?)", @@ -2104,12 +2179,13 @@ bool saveFacePhoto(const std::string& face_id, const std::string& photo_data) { sqliteBindText(stmt, 2, filepath); sqliteBindText(stmt, 3, "image/jpeg"); sqlite3_bind_int64(stmt, 4, static_cast(photo_data.size())); - sqliteBindText(stmt, 5, getCurrentTimestamp()); + sqliteBindText(stmt, 5, now); sqlite3_step(stmt); } sqlite3_finalize(stmt); - upsertFaceProfile(db, face_id, "", "active", getCurrentTimestamp()); + upsertFaceProfile(db, face_id, "", "active", now); + reconcileFaceProfilesWithPhotos(db, now); sqlite3_close(db); } std::cout << "[FACE_PHOTO] Saved: " << filepath << " (" << photo_data.size() << " bytes)" << std::endl; @@ -2150,23 +2226,6 @@ bool deleteFacePhoto(const std::string& face_id) { 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(); @@ -2184,40 +2243,20 @@ int deleteRecordsByStudentId(const std::string& student_id) { 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)); - } +std::vector findFaceIdsForStudent(sqlite3* db, const std::string& student_id) { + std::vector face_ids; + if (!db || student_id.empty()) return face_ids; + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, + "SELECT face_id FROM face_profiles WHERE student_id=? AND face_id<>'' ORDER BY face_id", + -1, &stmt, nullptr) == SQLITE_OK) { + sqliteBindText(stmt, 1, student_id); + while (sqlite3_step(stmt) == SQLITE_ROW) { + face_ids.push_back(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(); + sqlite3_finalize(stmt); + return face_ids; } bool updateDeviceSyncState(const std::string& client, unsigned long long seen_version, unsigned long long ack_version) { @@ -2362,6 +2401,25 @@ bool removeFromPendingDelete(const std::string& face_id_or_task_id) { return ok; } +bool cancelPendingDeleteForFace(sqlite3* db, const std::string& face_id, const std::string& now) { + if (!db || !isSafeId(face_id)) return false; + sqlite3_stmt* stmt = nullptr; + bool ok = false; + if (sqlite3_prepare_v2(db, + "UPDATE pending_delete SET status='acked',ack_at=? WHERE face_id=? AND status<>'acked'", + -1, &stmt, nullptr) == SQLITE_OK) { + sqliteBindText(stmt, 1, now); + sqliteBindText(stmt, 2, face_id); + ok = sqlite3_step(stmt) == SQLITE_DONE; + } + const int changed = sqlite3_changes(db); + sqlite3_finalize(stmt); + if (ok && changed > 0) { + std::cout << "[PENDING_DELETE] Canceled stale delete for face_id: " << face_id << std::endl; + } + return ok; +} + // ============= K210同步请求管理 ============= // 设置同步请求标志(QT端调用) bool setSyncRequest(bool sync) { @@ -2382,21 +2440,6 @@ 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}; @@ -2414,14 +2457,56 @@ void handleHttpRequest(SOCKET client_socket) { std::cout << "[HTTP] " << method << " " << path << std::endl; + 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); + } + std::string response_body; std::string content_type = "application/json"; int status_code = 200; std::string status_text = "OK"; auto sendResponse = [&]() { + bool response_encrypted = false; + const bool can_encrypt_response = method != "OPTIONS" && endpoint.find("/api/v1/") == 0 + && !isPublicEndpoint(method, endpoint) && !response_body.empty() + && isJsonLikeContentType(content_type); + if (can_encrypt_response) { + const std::string encrypted_body = encryptPayload(response_body); + if (encrypted_body.empty()) { + status_code = 500; + status_text = "Internal Server Error"; + response_body = makeJsonError("Encryption failed"); + content_type = "application/json"; + } else { + response_body = encrypted_body; + content_type = "application/octet-stream"; + response_encrypted = true; + } + } + + if (can_encrypt_response && !response_encrypted) { + const std::string fallback_body = encryptPayload(makeJsonError("Encryption failed")); + if (!fallback_body.empty()) { + response_body = fallback_body; + content_type = "application/octet-stream"; + response_encrypted = true; + } + } + 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-Type: " << content_type; + if (isJsonLikeContentType(content_type)) { + response << "; charset=utf-8"; + } + response << "\r\n"; + if (response_encrypted) { + response << "X-Encrypted: AES128-CBC\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"; @@ -2435,14 +2520,6 @@ void handleHttpRequest(SOCKET client_socket) { 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; @@ -2466,7 +2543,7 @@ void handleHttpRequest(SOCKET client_socket) { 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)) { + if (!parseIntInRange(cl_str, 0, static_cast(MAX_ENCRYPTED_PHOTO_BODY_SIZE), parsed_content_length)) { response_body = makeJsonError("Invalid Content-Length"); status_code = 400; status_text = "Bad Request"; @@ -2477,7 +2554,10 @@ void handleHttpRequest(SOCKET client_socket) { } 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; + const bool encrypted_body = isEncryptedRequest(request); + const size_t max_body_size = encrypted_body + ? (is_photo_upload ? MAX_ENCRYPTED_PHOTO_BODY_SIZE : MAX_ENCRYPTED_JSON_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; @@ -2519,6 +2599,36 @@ void handleHttpRequest(SOCKET client_socket) { sendResponse(); return; } + + if (!is_photo_upload && !encrypted_body) { + response_body = makeJsonError("Encrypted request body required"); + status_code = 400; + status_text = "Bad Request"; + sendResponse(); + return; + } + + if (encrypted_body) { + const std::string decrypted_body = decryptPayload(request_body); + if (decrypted_body.empty() && content_length > 0) { + response_body = makeJsonError("Decrypt request body failed"); + status_code = 400; + status_text = "Bad Request"; + sendResponse(); + return; + } + + if (decrypted_body.size() > (is_photo_upload ? MAX_PHOTO_BODY_SIZE : MAX_JSON_BODY_SIZE)) { + response_body = makeJsonError("Decrypted request body too large"); + status_code = 413; + status_text = "Payload Too Large"; + sendResponse(); + return; + } + + request_body = decrypted_body; + std::cout << "[HTTP] Decrypted POST body size: " << request_body.size() << " bytes" << std::endl; + } } // ============= 路由处理 ============= @@ -2543,73 +2653,50 @@ void handleHttpRequest(SOCKET client_socket) { 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; + std::cout << "[CHECKIN] Processing checkin data..." << std::endl; - // 检查是否是加密数据 - if (request.find("X-Encrypted: AES128-CBC") != std::string::npos) { - std::cout << "[CHECKIN] Encrypted data detected" << std::endl; + auto json_data = parseJSON(request_body); - // 解密数据 - std::string decrypted_json = decryptJSON(request_body); + if (json_data.find("student_id") != json_data.end()) { + std::string timestamp = getCurrentTimestamp(); + std::string student_id = json_data["student_id"]; + 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 (decrypted_json.empty()) { - response_body = "{\"success\":false,\"message\":\"解密失败\"}"; + 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; + + 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 { - // 解析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"; - } + response_body = "{\"success\":false,\"message\":\"数据库写入失败\"}"; + status_code = 500; + status_text = "Internal Server Error"; } } else { - response_body = "{\"success\":false,\"message\":\"未加密数据\"}"; + 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; @@ -2624,33 +2711,11 @@ void handleHttpRequest(SOCKET client_socket) { 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; - } + response_body = readDatabase(limit, offset); } } 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; - } + response_body = getStatistics(); } else if (method == "POST" && endpoint == "/api/v1/attendance/records/clear") { if (clearDatabase()) { @@ -2778,26 +2843,48 @@ void handleHttpRequest(SOCKET client_socket) { 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; + } else if (method == "GET" && endpoint == "/api/v1/students") { + response_body = readStudents(); + std::cout << "[STUDENTS] List fetched" << std::endl; - if (saveMappingConfig(request_body)) { - response_body = "{\"success\":true,\"message\":\"映射配置已保存\"}"; - bumpSyncVersion("mapping_config"); - std::cout << "[MAPPING] ✓ Configuration saved successfully" << std::endl; + } else if (method == "POST" && endpoint == "/api/v1/students") { + if (saveStudents(request_body)) { + response_body = "{\"success\":true,\"message\":\"学生档案已保存\"}"; + bumpSyncVersion("students"); } else { - response_body = "{\"success\":false,\"message\":\"保存映射配置失败\"}"; - status_code = 500; - status_text = "Internal Server Error"; + response_body = "{\"success\":false,\"message\":\"保存学生档案失败\"}"; + status_code = 400; + status_text = "Bad Request"; } - } else if (method == "GET" && endpoint == "/api/v1/config/mappings") { - // 获取映射配置: GET /api/v1/config/mappings - std::cout << "[MAPPING] Reading configuration" << std::endl; + } else if (method == "GET" && endpoint == "/api/v1/classes") { + response_body = readClasses(); + std::cout << "[CLASSES] List fetched" << std::endl; - response_body = readMappingConfig(); - std::cout << "[MAPPING] Returning config: " << response_body.size() << " bytes" << std::endl; + } else if (method == "POST" && endpoint == "/api/v1/classes") { + if (saveClasses(request_body)) { + response_body = "{\"success\":true,\"message\":\"班级配置已保存\"}"; + bumpSyncVersion("classes"); + } else { + response_body = "{\"success\":false,\"message\":\"保存班级失败,请确认被删除班级下没有学生\"}"; + status_code = 400; + status_text = "Bad Request"; + } + + } else if (method == "GET" && endpoint == "/api/v1/face_profiles") { + response_body = readFaceProfiles(); + std::cout << "[FACE_PROFILE] List fetched" << std::endl; + + } else if (method == "POST" && endpoint == "/api/v1/face_profiles") { + if (saveFaceProfiles(request_body)) { + response_body = "{\"success\":true,\"message\":\"人脸档案已保存,已触发 K210 同步\"}"; + bumpSyncVersion("face_profiles"); + setSyncRequest(true); + } else { + response_body = "{\"success\":false,\"message\":\"保存人脸档案失败\"}"; + status_code = 400; + status_text = "Bad Request"; + } // ============= 人脸照片管理 API ============= } else if (method == "GET" && endpoint == "/api/v1/faces/photos") { @@ -2845,67 +2932,46 @@ void handleHttpRequest(SOCKET client_socket) { 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())); + // 删除人脸信息: DELETE /api/v1/faces/?delete_records=true + std::string requested_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 + std::cout << "[DELETE_FACE] Deleting face for id: " << requested_id << ", delete_records: " << (delete_records ? "yes" : "no") << std::endl; - if (!isSafeId(student_id)) { - response_body = "{\"success\":false,\"message\":\"Invalid student_id\"}"; + if (!isSafeId(requested_id)) { + response_body = "{\"success\":false,\"message\":\"Invalid 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); + std::vector face_ids; + sqlite3* db = openSqliteDb(); + if (db) { + if (facePhotoExists(db, requested_id) || faceProfileExists(db, requested_id)) { + face_ids.push_back(requested_id); + } else { + face_ids = findFaceIdsForStudent(db, requested_id); + } + sqlite3_close(db); + } + if (face_ids.empty()) { + face_ids.push_back(requested_id); + } + + bool photo_deleted = false; int records_deleted = 0; if (delete_records) { - for (const auto& id : student_ids) { - records_deleted += deleteRecordsByStudentId(id); - } + records_deleted = deleteRecordsByStudentId(requested_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; + for (const auto& face_id : face_ids) { + photo_deleted = deleteFacePhoto(face_id) || photo_deleted; + pending_added = addToPendingDelete(face_id, requested_id) || pending_added; } bumpSyncVersion("delete_face"); setSyncRequest(true); @@ -2913,7 +2979,7 @@ void handleHttpRequest(SOCKET client_socket) { std::ostringstream result; result << "{\"success\":true," << "\"photo_deleted\":" << (photo_deleted ? "true" : "false") << "," - << "\"mapping_deleted\":" << (mapping_deleted ? "true" : "false") << "," + << "\"profile_unbound\":true," << "\"records_deleted\":" << records_deleted << "," << "\"k210_pending\":" << (pending_added ? "true" : "false") << "," << "\"sync_requested\":true}"; @@ -2922,30 +2988,6 @@ void handleHttpRequest(SOCKET client_socket) { 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查询是否需要同步 @@ -2963,17 +3005,6 @@ void handleHttpRequest(SOCKET client_socket) { 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;