硬件基本完成

This commit is contained in:
2026-05-01 21:34:10 +08:00
parent df5b05e936
commit 5a31ff2179
14 changed files with 1005 additions and 439 deletions
+352 -67
View File
@@ -63,9 +63,10 @@ const std::string FACES_DIR = "faces"; // 人脸照片目录
const std::string PENDING_DELETE_FILE = "pending_delete.json"; // 待删除人脸列表
const std::string SYNC_REQUEST_FILE = "sync_request.json"; // K210同步请求标志文件
const std::string SYNC_VERSION_FILE = "sync_version.txt"; // 全局同步版本号
const std::string DB_BACKUP_DIR = "backups";
const size_t MAX_JSON_BODY_SIZE = 64 * 1024;
const size_t MAX_PHOTO_BODY_SIZE = 512 * 1024;
const char* DEFAULT_API_TOKEN = "change-me-token";
const char* DEFAULT_API_TOKEN = "atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV";
const char* DEFAULT_CORS_ORIGIN = "http://localhost";
const char* DEFAULT_TIMESLOTS_JSON =
"{\"time_slots\":["
@@ -93,12 +94,19 @@ const std::string& allowedCorsOrigin() {
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,
@@ -704,39 +712,90 @@ std::map<std::string, std::string> parseQueryParams(const std::string& query) {
return params;
}
// 简单的JSON解析
std::map<std::string, std::string> parseJSON(const std::string& json) {
std::map<std::string, std::string> result;
bool parseJsonStringLiteral(const std::string& json, size_t& pos, std::string& output) {
if (pos >= json.size() || json[pos] != '"') return false;
pos++;
output.clear();
std::string content = json;
size_t start = content.find('{');
size_t end = content.rfind('}');
if (start != std::string::npos && end != std::string::npos) {
content = content.substr(start + 1, end - start - 1);
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;
}
}
std::istringstream ss(content);
std::string token;
return false;
}
while (std::getline(ss, token, ',')) {
size_t colon = token.find(':');
if (colon != std::string::npos) {
std::string key = token.substr(0, colon);
std::string value = token.substr(colon + 1);
// 解析签到接口使用的扁平JSON对象,支持字符串转义、逗号和冒号。
std::map<std::string, std::string> parseJSON(const std::string& json) {
std::map<std::string, std::string> result;
size_t pos = json.find('{');
if (pos == std::string::npos) return result;
pos++;
// 移除引号和空格
key.erase(std::remove_if(key.begin(), key.end(),
[](char c) { return c == '\"' || c == ' ' || c == '\t'; }), key.end());
value.erase(std::remove_if(value.begin(), value.end(),
[](char c) { return c == '\"'; }), value.end());
while (pos < json.size()) {
while (pos < json.size() && std::isspace(static_cast<unsigned char>(json[pos]))) pos++;
if (pos >= json.size() || json[pos] == '}') break;
// 保留timestamp中的空格
if (key != "timestamp") {
value.erase(std::remove(value.begin(), value.end(), ' '), value.end());
}
result[key] = value;
std::string key;
if (!parseJsonStringLiteral(json, pos, key)) {
result.clear();
return result;
}
while (pos < json.size() && std::isspace(static_cast<unsigned char>(json[pos]))) pos++;
if (pos >= json.size() || json[pos] != ':') {
result.clear();
return result;
}
pos++;
while (pos < json.size() && std::isspace(static_cast<unsigned char>(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<unsigned char>(json[pos]))) pos++;
if (pos < json.size() && json[pos] == ',') pos++;
}
return result;
@@ -784,6 +843,54 @@ bool sqliteTableHasRows(sqlite3* db, const std::string& table) {
return has_rows;
}
bool sqliteTableHasColumn(sqlite3* db, const std::string& table, const std::string& column) {
sqlite3_stmt* stmt = nullptr;
bool found = false;
std::string sql = "PRAGMA table_info(" + table + ")";
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
if (sqliteColumnText(stmt, 1) == column) {
found = true;
break;
}
}
}
sqlite3_finalize(stmt);
return found;
}
bool copyFileBinary(const std::string& src, const std::string& dst) {
std::ifstream in(src.c_str(), std::ios::binary);
if (!in.is_open()) return false;
std::ofstream out(dst.c_str(), std::ios::binary);
if (!out.is_open()) return false;
out << in.rdbuf();
return out.good();
}
bool backupSqliteDatabaseOnStartup() {
std::ifstream check(SQLITE_DB_FILE.c_str(), std::ios::binary);
if (!check.is_open()) {
return true;
}
check.close();
_mkdir(DB_BACKUP_DIR.c_str());
std::string ts = getCurrentTimestamp();
std::replace(ts.begin(), ts.end(), ':', '-');
std::replace(ts.begin(), ts.end(), ' ', '_');
const std::string backup_path = DB_BACKUP_DIR + "/attendance_" + ts + ".db";
if (!copyFileBinary(SQLITE_DB_FILE, backup_path)) {
std::cerr << "[BACKUP] Failed to create SQLite backup: " << backup_path << std::endl;
return false;
}
std::cout << "[BACKUP] SQLite backup created: " << backup_path << std::endl;
return true;
}
std::string readWholeFileOrDefault(const std::string& path, const std::string& fallback) {
std::ifstream file(path.c_str(), std::ios::binary);
if (!file.is_open()) return fallback;
@@ -968,7 +1075,19 @@ bool initializeSqliteStorage() {
"student_id TEXT NOT NULL"
");"
"CREATE TABLE IF NOT EXISTS pending_delete("
"face_id TEXT PRIMARY KEY"
"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) {
@@ -976,6 +1095,25 @@ 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='legacy-' || face_id WHERE task_id IS NULL OR task_id=''");
sqliteExec(db, "UPDATE pending_delete SET created_at='" + getCurrentTimestamp() + "' WHERE created_at IS NULL OR created_at=''");
sqliteExec(db, "UPDATE pending_delete SET status='pending' WHERE status IS NULL OR status=''");
if (!sqliteTableHasRows(db, "records")) {
std::ifstream csv(DB_FILE.c_str());
if (csv.is_open()) {
@@ -1037,12 +1175,16 @@ bool initializeSqliteStorage() {
std::vector<std::string> pending = parseJsonStringList(
readWholeFileOrDefault(PENDING_DELETE_FILE, "{\"pending_delete\":[]}"), "pending_delete");
sqlite3_stmt* stmt = nullptr;
const char* sql = "REPLACE INTO pending_delete(face_id) VALUES(?)";
const char* sql = "REPLACE INTO pending_delete(task_id,face_id,student_id,created_at,status) VALUES(?,?,?,?,?)";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
for (const auto& id : pending) {
sqlite3_reset(stmt);
sqlite3_clear_bindings(stmt);
sqliteBindText(stmt, 1, id);
sqliteBindText(stmt, 1, "legacy-" + id);
sqliteBindText(stmt, 2, id);
sqliteBindText(stmt, 3, id);
sqliteBindText(stmt, 4, getCurrentTimestamp());
sqliteBindText(stmt, 5, "pending");
sqlite3_step(stmt);
}
}
@@ -1280,9 +1422,26 @@ std::vector<std::string> extractJsonStringArray(const std::string& json, const s
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<unsigned char>(json[pos]))) pos++;
unsigned long long value = 0;
bool has_digit = false;
while (pos < json.size() && std::isdigit(static_cast<unsigned char>(json[pos]))) {
has_digit = true;
value = value * 10ULL + static_cast<unsigned long long>(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
@@ -1296,9 +1455,12 @@ std::string makeSyncStateResponse(const std::string& client, unsigned long long
if (client == "k210" || client == "qt" || client == "all" || client.empty()) {
const std::string mappings = extractJsonMemberValue(readFaceIdMapping(), "mappings", "{}");
const std::string pending = extractJsonMemberValue(getPendingDeleteList(), "pending_delete", "[]");
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;
<< ",\"pending_delete\":" << pending
<< ",\"delete_tasks\":" << delete_tasks;
first = false;
}
@@ -1328,15 +1490,25 @@ std::string makeSyncStateResponse(const std::string& client, unsigned long long
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<unsigned long long>(jsonNumberValue(request_body, "version", 0));
const std::vector<std::string> deleted = extractJsonStringArray(request_body, "deleted");
const std::vector<std::string> 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");
}
@@ -1385,6 +1557,7 @@ std::string makeStorageStatusResponse() {
<< "\"face_photos\":" << face_photos << ","
<< "\"pending_deletes\":" << pending_deletes << ","
<< "\"sync_version\":" << readSyncVersion() << ","
<< "\"device_sync_state\":" << getDeviceSyncSummary() << ","
<< "\"server_time\":\"" << jsonEscape(getCurrentTimestamp()) << "\""
<< "}";
return json.str();
@@ -1824,56 +1997,145 @@ std::string getAllStudentIds() {
return json.str();
}
// 添加人脸ID到待删除列表
bool addToPendingDelete(const std::string& face_id) {
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;
if (sqlite3_prepare_v2(db, "REPLACE INTO pending_delete(face_id) VALUES(?)", -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, face_id);
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<sqlite3_int64>(seen_version));
sqlite3_bind_int64(stmt, 3, static_cast<sqlite3_int64>(ack_version));
sqliteBindText(stmt, 4, getCurrentTimestamp());
ok = sqlite3_step(stmt) == SQLITE_DONE;
}
sqlite3_finalize(stmt);
sqlite3_close(db);
std::cout << "[PENDING_DELETE] Added: " << face_id << std::endl;
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<long long>(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\":[]}";
if (!db) return "{\"pending_delete\":[],\"delete_tasks\":[]}";
sqlite3_stmt* stmt = nullptr;
std::ostringstream json;
json << "{\"pending_delete\":[";
bool first = true;
if (sqlite3_prepare_v2(db, "SELECT face_id FROM pending_delete ORDER BY face_id", -1, &stmt, nullptr) == SQLITE_OK) {
std::vector<std::map<std::string, std::string>> 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) {
if (!first) json << ",";
json << "\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\"";
first = false;
std::map<std::string, std::string> 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) {
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, "DELETE FROM pending_delete WHERE face_id=?", -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, face_id);
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] Removed: " << face_id << std::endl;
std::cout << "[PENDING_DELETE] Acked: " << face_id_or_task_id << std::endl;
return ok;
}
@@ -2078,10 +2340,23 @@ void handleHttpRequest(SOCKET client_socket) {
// 构建考勤记录
std::string timestamp = getCurrentTimestamp();
std::string student_id = json_data["student_id"]; // K210已发送真实学号
std::string name = json_data["name"];
std::string confidence = json_data["confidence"];
std::string status = json_data["status"];
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;
@@ -2404,7 +2679,7 @@ void handleHttpRequest(SOCKET client_socket) {
photo_deleted = deleteFacePhoto(face_id) || photo_deleted;
mapping_deleted = removeFaceIdFromMapping(face_id) || mapping_deleted;
}
pending_added = addToPendingDelete(face_id) || pending_added;
pending_added = addToPendingDelete(face_id, student_id) || pending_added;
}
for (const auto& id : student_ids) {
photo_deleted = deleteFacePhoto(id) || photo_deleted;
@@ -2431,21 +2706,17 @@ void handleHttpRequest(SOCKET client_socket) {
} else if (method == "POST" && endpoint == "/api/confirm_delete") {
// K210确认删除完成,从待删除列表移除
// 请求体格式: {"face_id":"xxx"}
std::string face_id;
size_t pos = request_body.find("\"face_id\":\"");
if (pos != std::string::npos) {
size_t start = pos + 11;
size_t end = request_body.find("\"", start);
if (end != std::string::npos) {
face_id = request_body.substr(start, end - start);
}
// 请求体格式: {"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 (!face_id.empty() && removeFromPendingDelete(face_id)) {
response_body = "{\"success\":true,\"message\":\"Confirmed delete: " + jsonEscape(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: " << face_id << std::endl;
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;
@@ -2502,7 +2773,16 @@ void httpServerThread() {
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
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) {
@@ -2512,7 +2792,7 @@ void httpServerThread() {
}
listen(server_socket, 10);
std::cout << "[HTTP] Server listening on port " << HTTP_PORT << std::endl;
std::cout << "[HTTP] Server listening on " << bind_host << ":" << HTTP_PORT << std::endl;
while (true) {
sockaddr_in client_addr;
@@ -2539,6 +2819,11 @@ int main() {
std::cout << "Database: " << SQLITE_DB_FILE << std::endl;
std::cout << "----------------------------------------" << std::endl;
if (!backupSqliteDatabaseOnStartup()) {
std::cerr << "[BACKUP] Startup backup failed, stop to avoid unsafe writes" << std::endl;
return 1;
}
// 初始化SQLite,并首次导入旧CSV/JSON配置
if (!initializeSqliteStorage()) {
std::cerr << "[SQLITE] Failed to initialize storage" << std::endl;