Files
bishe-STM32/server/server_encrypted.cpp
T
2026-05-01 20:29:02 +08:00

2559 lines
94 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 增强版考勤服务器 - 支持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 <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <thread>
#include <vector>
#include <map>
#include <set>
#include <ctime>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <cstdio>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <direct.h>
#pragma comment(lib, "ws2_32.lib")
typedef int socklen_t;
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <cstring>
#include <dirent.h>
#include <sys/stat.h>
#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 <openssl/evp.h>
#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/aes.h>
#include <sqlite3.h>
const int HTTP_PORT = 8082;
const int BUFFER_SIZE = 4096;
const std::string SQLITE_DB_FILE = "attendance.db";
const std::string LOG_FILE = "attendance.log";
const std::string DB_FILE = "attendance_database.csv";
const std::string TIMESLOTS_FILE = "timeslots_config.json"; // 时间段配置文件
const std::string MAPPING_CONFIG_FILE = "mapping_config.json"; // 映射配置文件
const std::string FACE_ID_MAPPING_FILE = "face_id_mapping.json"; // 人脸ID映射文件
const std::string FACES_DIR = "faces"; // 人脸照片目录
const std::string PENDING_DELETE_FILE = "pending_delete.json"; // 待删除人脸列表
const std::string SYNC_REQUEST_FILE = "sync_request.json"; // K210同步请求标志文件
const std::string SYNC_VERSION_FILE = "sync_version.txt"; // 全局同步版本号
const 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_CORS_ORIGIN = "http://localhost";
const char* DEFAULT_TIMESLOTS_JSON =
"{\"time_slots\":["
"{\"name\":\"上午签到\",\"start_time\":\"08:00\",\"end_time\":\"09:30\",\"enabled\":true},"
"{\"name\":\"中午签退\",\"start_time\":\"12:00\",\"end_time\":\"13:00\",\"enabled\":true},"
"{\"name\":\"下午签到\",\"start_time\":\"14:00\",\"end_time\":\"15:00\",\"enabled\":true},"
"{\"name\":\"晚上签退\",\"start_time\":\"17:00\",\"end_time\":\"18:30\",\"enabled\":true}"
"]}";
std::string getEnvOrDefault(const char* name, const char* fallback) {
const char* value = std::getenv(name);
if (value && value[0] != '\0') {
return std::string(value);
}
return std::string(fallback);
}
const std::string& apiToken() {
static const std::string token = getEnvOrDefault("ATTENDANCE_API_TOKEN", DEFAULT_API_TOKEN);
return token;
}
const std::string& allowedCorsOrigin() {
static const std::string origin = getEnvOrDefault("ATTENDANCE_CORS_ORIGIN", DEFAULT_CORS_ORIGIN);
return origin;
}
std::string readFaceIdMapping();
std::string getPendingDeleteList();
std::string getFacePhotosList();
std::string getAllStudentIds();
bool setSyncRequest(bool sync);
bool removeFromPendingDelete(const std::string& face_id);
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<std::string, std::string> parseFaceIdMappings(const std::string& json_data);
std::string jsonEscape(const std::string& value) {
std::ostringstream out;
for (unsigned char c : value) {
switch (c) {
case '\"': out << "\\\""; break;
case '\\': out << "\\\\"; break;
case '\b': out << "\\b"; break;
case '\f': out << "\\f"; break;
case '\n': out << "\\n"; break;
case '\r': out << "\\r"; break;
case '\t': out << "\\t"; break;
default:
if (c < 0x20) {
const char* hex = "0123456789abcdef";
out << "\\u00" << hex[(c >> 4) & 0x0f] << hex[c & 0x0f];
} else {
out << static_cast<char>(c);
}
break;
}
}
return out.str();
}
std::string csvEscape(const std::string& value) {
bool needs_quotes = false;
for (char c : value) {
if (c == ',' || c == '"' || c == '\r' || c == '\n') {
needs_quotes = true;
break;
}
}
if (!needs_quotes) {
return value;
}
std::string escaped = "\"";
for (char c : value) {
if (c == '"') {
escaped += "\"\"";
} else {
escaped += c;
}
}
escaped += "\"";
return escaped;
}
std::vector<std::string> parseCsvLine(const std::string& line) {
std::vector<std::string> fields;
std::string current;
bool in_quotes = false;
for (size_t i = 0; i < line.size(); ++i) {
char c = line[i];
if (in_quotes) {
if (c == '"' && i + 1 < line.size() && line[i + 1] == '"') {
current += '"';
++i;
} else if (c == '"') {
in_quotes = false;
} else {
current += c;
}
} else if (c == '"') {
in_quotes = true;
} else if (c == ',') {
fields.push_back(current);
current.clear();
} else {
current += c;
}
}
fields.push_back(current);
return fields;
}
bool parseIntInRange(const std::string& value, int min_value, int max_value, int& output) {
if (value.empty()) {
return false;
}
try {
size_t pos = 0;
int parsed = std::stoi(value, &pos);
if (pos != value.size() || parsed < min_value || parsed > max_value) {
return false;
}
output = parsed;
return true;
} catch (...) {
return false;
}
}
bool parseTimeParts(const std::string& value, int& hour, int& minute) {
if (value.size() != 5 || value[2] != ':'
|| !std::isdigit(static_cast<unsigned char>(value[0]))
|| !std::isdigit(static_cast<unsigned char>(value[1]))
|| !std::isdigit(static_cast<unsigned char>(value[3]))
|| !std::isdigit(static_cast<unsigned char>(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<char>(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<char>(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<char>(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<char>(std::tolower(c)); });
if (key == expected_key && value == expected) {
return true;
}
}
return false;
}
bool isPublicEndpoint(const std::string& method, const std::string& endpoint) {
return method == "OPTIONS" || (method == "GET" && endpoint == "/api/health");
}
std::string normalizeApiEndpoint(const std::string& method, const std::string& endpoint) {
if (endpoint.find("/api/v1/") != 0) {
return endpoint;
}
if (endpoint == "/api/v1/system/health") return "/api/health";
if (endpoint == "/api/v1/system/time") return "/api/time";
if (method == "POST" && endpoint == "/api/v1/attendance/checkins") return "/api/checkin";
if (method == "GET" && endpoint == "/api/v1/attendance/records") return "/api/records";
if (method == "DELETE" && endpoint == "/api/v1/attendance/records") return "/api/record";
if (method == "POST" && endpoint == "/api/v1/attendance/records/clear") return "/api/clear";
if (method == "GET" && endpoint == "/api/v1/attendance/statistics") return "/api/statistics";
if (method == "GET" && endpoint == "/api/v1/attendance/duplicates") return "/api/check_duplicate";
if (method == "GET" && endpoint == "/api/v1/attendance/today") return "/api/today_records";
if (endpoint == "/api/v1/config/time-slots") return "/api/timeslots";
if (endpoint == "/api/v1/config/mappings") return "/api/mapping_config";
if (endpoint == "/api/v1/faces/mappings") return "/api/face_id_mapping";
if (endpoint == "/api/v1/faces/photos") return "/api/face_photos";
if (endpoint.find("/api/v1/faces/photos/") == 0) {
const std::string id = endpoint.substr(std::string("/api/v1/faces/photos/").size());
return method == "POST" ? "/api/face_photo_simple/" + id : "/api/face_photo/" + id;
}
if (endpoint == "/api/v1/faces/student-ids") return "/api/all_student_ids";
if (endpoint == "/api/v1/faces/pending-delete") return "/api/pending_delete";
if (method == "POST" && endpoint == "/api/v1/faces/delete-ack") return "/api/confirm_delete";
if (method == "DELETE" && endpoint.find("/api/v1/faces/") == 0) {
return "/api/face/" + endpoint.substr(std::string("/api/v1/faces/").size());
}
if (endpoint == "/api/v1/sync/request") return "/api/sync_request";
if (endpoint == "/api/v1/sync/request/ack") return "/api/sync_ack";
if (endpoint == "/api/v1/sync/trigger") return "/api/trigger_sync";
return endpoint;
}
bool isAuthorizedRequest(const std::string& request, const std::string& method, const std::string& endpoint) {
if (isPublicEndpoint(method, endpoint)) {
return true;
}
return hasHeaderToken(request, "X-Api-Token", apiToken());
}
std::string makeJsonError(const std::string& message) {
return "{\"success\":false,\"message\":\"" + jsonEscape(message) + "\"}";
}
// ⚠️ 重要:密钥和IV必须与STM32端完全一致!
static const unsigned char MY_AES_KEY[16] = {
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c
};
static const unsigned char MY_AES_IV[16] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
};
// ============= Base64解码 =============
std::string base64_decode(const std::string& encoded_string) {
BIO *bio, *b64;
int decode_len = encoded_string.length();
char* buffer = new char[decode_len];
bio = BIO_new_mem_buf(encoded_string.c_str(), -1);
b64 = BIO_new(BIO_f_base64());
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
int length = BIO_read(bio, buffer, decode_len);
BIO_free_all(bio);
std::string result(buffer, length);
delete[] buffer;
return result;
}
// ============= 添加PKCS#7填充 =============
std::string add_pkcs7_padding(const std::string& data) {
size_t padding_len = 16 - (data.length() % 16);
std::string result = data;
for (size_t i = 0; i < padding_len; i++) {
result += (char)padding_len;
}
return result;
}
// ============= 手动移除PKCS#7填充 =============
std::string remove_pkcs7_padding(const std::string& data) {
if (data.empty()) return "";
// 获取最后一个字节(填充长度)
unsigned char padding_len = (unsigned char)data[data.length() - 1];
// 验证填充长度
if (padding_len < 1 || padding_len > 16 || padding_len > data.length()) {
std::cerr << "[CRYPTO] Invalid padding length: " << (int)padding_len << std::endl;
return data; // 返回原数据
}
// 验证所有填充字节都相同
for (size_t i = data.length() - padding_len; i < data.length(); i++) {
if ((unsigned char)data[i] != padding_len) {
std::cerr << "[CRYPTO] Invalid padding at position " << i << std::endl;
return data; // 返回原数据
}
}
// 移除填充
return data.substr(0, data.length() - padding_len);
}
// ============= AES-CBC加密 (用于响应Qt客户端) =============
std::string aes_encrypt_cbc(const std::string& plaintext) {
// 添加PKCS#7填充
std::string padded = add_pkcs7_padding(plaintext);
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) {
std::cerr << "[CRYPTO] Failed to create context" << std::endl;
return "";
}
// 初始化加密
if (EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, MY_AES_KEY, MY_AES_IV) != 1) {
std::cerr << "[CRYPTO] EncryptInit failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
// 禁用自动填充(我们已手动添加)
EVP_CIPHER_CTX_set_padding(ctx, 0);
// 加密数据
std::vector<unsigned char> 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<unsigned char> 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(&ltm, &now);
#else
localtime_r(&now, &ltm);
#endif
char buffer[80];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &ltm);
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<char>(value);
i += 2;
} else {
result += str[i];
}
} else if (str[i] == '+') {
result += ' ';
} else {
result += str[i];
}
}
return result;
}
std::map<std::string, std::string> parseQueryParams(const std::string& query) {
std::map<std::string, std::string> 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;
}
// 简单的JSON解析
std::map<std::string, std::string> parseJSON(const std::string& json) {
std::map<std::string, std::string> result;
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);
}
std::istringstream ss(content);
std::string token;
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);
// 移除引号和空格
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());
// 保留timestamp中的空格
if (key != "timestamp") {
value.erase(std::remove(value.begin(), value.end(), ' '), value.end());
}
result[key] = value;
}
}
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<const char*>(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;
}
std::string readWholeFileOrDefault(const std::string& path, const std::string& fallback) {
std::ifstream file(path.c_str(), std::ios::binary);
if (!file.is_open()) return fallback;
std::stringstream buffer;
buffer << file.rdbuf();
std::string content = buffer.str();
return content.empty() ? fallback : content;
}
bool sqliteSetKv(sqlite3* db, const std::string& key, const std::string& value) {
sqlite3_stmt* stmt = nullptr;
const char* sql = "REPLACE INTO kv_store(key,value) VALUES(?,?)";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false;
sqliteBindText(stmt, 1, key);
sqliteBindText(stmt, 2, value);
bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return ok;
}
std::string sqliteGetKv(sqlite3* db, const std::string& key, const std::string& fallback) {
sqlite3_stmt* stmt = nullptr;
std::string value = fallback;
const char* sql = "SELECT value FROM kv_store WHERE key=?";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, key);
if (sqlite3_step(stmt) == SQLITE_ROW) {
value = sqliteColumnText(stmt, 0);
}
}
sqlite3_finalize(stmt);
return value;
}
bool sqliteSetKvValue(const std::string& key, const std::string& value) {
sqlite3* db = openSqliteDb();
if (!db) return false;
bool ok = sqliteSetKv(db, key, value);
sqlite3_close(db);
return ok;
}
std::string sqliteGetKvValue(const std::string& key, const std::string& fallback) {
sqlite3* db = openSqliteDb();
if (!db) return fallback;
std::string value = sqliteGetKv(db, key, fallback);
sqlite3_close(db);
return value;
}
int sqliteCountRows(sqlite3* db, const std::string& table) {
sqlite3_stmt* stmt = nullptr;
std::string sql = "SELECT COUNT(*) FROM " + table;
int count = 0;
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK
&& sqlite3_step(stmt) == SQLITE_ROW) {
count = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
return count;
}
bool sqliteQuickCheck(std::string& message) {
sqlite3* db = openSqliteDb();
if (!db) {
message = "open_failed";
return false;
}
sqlite3_stmt* stmt = nullptr;
bool ok = false;
message = "unknown";
if (sqlite3_prepare_v2(db, "PRAGMA quick_check", -1, &stmt, nullptr) == SQLITE_OK
&& sqlite3_step(stmt) == SQLITE_ROW) {
message = sqliteColumnText(stmt, 0);
ok = message == "ok";
}
sqlite3_finalize(stmt);
sqlite3_close(db);
return ok;
}
int countFacePhotoFiles() {
int count = 0;
#ifdef _WIN32
WIN32_FIND_DATAA findData;
std::string searchPath = FACES_DIR + "\\*.jpg";
HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData);
if (hFind != INVALID_HANDLE_VALUE) {
do {
count++;
} while (FindNextFileA(hFind, &findData));
FindClose(hFind);
}
#else
DIR* dir = opendir(FACES_DIR.c_str());
if (dir) {
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
std::string name = entry->d_name;
if (name.size() > 4 && name.substr(name.size() - 4) == ".jpg") {
count++;
}
}
closedir(dir);
}
#endif
return count;
}
bool sqliteInsertRecord(sqlite3* db, const std::string& timestamp, const std::string& student_id,
const std::string& name, const std::string& confidence,
const std::string& status, const std::string& class_id) {
sqlite3_stmt* stmt = nullptr;
const char* sql =
"INSERT INTO records(timestamp,student_id,name,confidence,status,class_id) "
"VALUES(?,?,?,?,?,?)";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false;
sqliteBindText(stmt, 1, timestamp);
sqliteBindText(stmt, 2, student_id);
sqliteBindText(stmt, 3, name);
sqliteBindText(stmt, 4, confidence);
sqliteBindText(stmt, 5, status);
sqliteBindText(stmt, 6, class_id.empty() ? "0" : class_id);
bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return ok;
}
bool saveAttendanceRecord(const std::string& timestamp, const std::string& student_id,
const std::string& name, const std::string& confidence,
const std::string& status, const std::string& class_id) {
sqlite3* db = openSqliteDb();
if (!db) return false;
bool ok = sqliteInsertRecord(db, timestamp, student_id, name, confidence, status, class_id);
sqlite3_close(db);
return ok;
}
std::vector<std::string> parseJsonStringList(const std::string& json, const std::string& key) {
std::vector<std::string> values;
std::string marker = "\"" + key + "\":[";
size_t start = json.find(marker);
if (start == std::string::npos) return values;
start = json.find('[', start);
size_t end = json.find(']', start);
if (start == std::string::npos || end == std::string::npos) return values;
std::string arr = json.substr(start + 1, end - start - 1);
std::istringstream ss(arr);
std::string token;
while (std::getline(ss, token, ',')) {
token.erase(std::remove(token.begin(), token.end(), '"'), token.end());
token.erase(std::remove_if(token.begin(), token.end(),
[](unsigned char c) { return std::isspace(c); }), token.end());
if (!token.empty()) values.push_back(token);
}
return values;
}
bool initializeSqliteStorage() {
sqlite3* db = openSqliteDb();
if (!db) return false;
bool ok = sqliteExec(db,
"PRAGMA journal_mode=WAL;"
"CREATE TABLE IF NOT EXISTS records("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"timestamp TEXT NOT NULL,"
"student_id TEXT NOT NULL,"
"name TEXT,"
"confidence TEXT,"
"status TEXT,"
"class_id TEXT DEFAULT '0'"
");"
"CREATE INDEX IF NOT EXISTS idx_records_student_time ON records(student_id,timestamp);"
"CREATE INDEX IF NOT EXISTS idx_records_timestamp ON records(timestamp);"
"CREATE TABLE IF NOT EXISTS kv_store("
"key TEXT PRIMARY KEY,"
"value TEXT NOT NULL"
");"
"CREATE TABLE IF NOT EXISTS face_mappings("
"face_id TEXT PRIMARY KEY,"
"student_id TEXT NOT NULL"
");"
"CREATE TABLE IF NOT EXISTS pending_delete("
"face_id TEXT PRIMARY KEY"
");");
if (!ok) {
sqlite3_close(db);
return false;
}
if (!sqliteTableHasRows(db, "records")) {
std::ifstream csv(DB_FILE.c_str());
if (csv.is_open()) {
std::string line;
std::getline(csv, line);
int imported = 0;
sqliteExec(db, "BEGIN");
while (std::getline(csv, line)) {
if (line.empty()) continue;
std::vector<std::string> fields = parseCsvLine(line);
std::string timestamp = fields.size() > 0 ? fields[0] : "";
std::string student_id = fields.size() > 1 ? fields[1] : "";
std::string name = fields.size() > 2 ? fields[2] : "";
std::string confidence = fields.size() > 3 ? fields[3] : "0";
std::string status = fields.size() > 4 ? fields[4] : "";
std::string class_id = fields.size() > 5 ? fields[5] : "0";
if (!timestamp.empty() && !student_id.empty()
&& sqliteInsertRecord(db, timestamp, student_id, name, confidence, status, class_id)) {
imported++;
}
}
sqliteExec(db, "COMMIT");
std::cout << "[SQLITE] Imported records from CSV: " << imported << std::endl;
}
}
if (sqliteGetKv(db, "time_slots", "").empty()) {
sqliteSetKv(db, "time_slots", readWholeFileOrDefault(TIMESLOTS_FILE, DEFAULT_TIMESLOTS_JSON));
}
if (sqliteGetKv(db, "mapping_config", "").empty()) {
sqliteSetKv(db, "mapping_config", readWholeFileOrDefault(MAPPING_CONFIG_FILE, "{\"student_name_map\":{},\"class_name_map\":{}}"));
}
if (sqliteGetKv(db, "sync_request", "").empty()) {
sqliteSetKv(db, "sync_request", readWholeFileOrDefault(SYNC_REQUEST_FILE, "{\"sync\":false}"));
}
if (sqliteGetKv(db, "sync_version", "").empty()) {
sqliteSetKv(db, "sync_version", readWholeFileOrDefault(SYNC_VERSION_FILE, "1"));
}
if (!sqliteTableHasRows(db, "face_mappings")) {
std::map<std::string, std::string> mappings = parseFaceIdMappings(
readWholeFileOrDefault(FACE_ID_MAPPING_FILE, "{\"mappings\":{}}"));
sqlite3_stmt* stmt = nullptr;
const char* sql = "REPLACE INTO face_mappings(face_id,student_id) VALUES(?,?)";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
for (const auto& m : mappings) {
sqlite3_reset(stmt);
sqlite3_clear_bindings(stmt);
sqliteBindText(stmt, 1, m.first);
sqliteBindText(stmt, 2, m.second);
sqlite3_step(stmt);
}
}
sqlite3_finalize(stmt);
std::cout << "[SQLITE] Imported face mappings: " << mappings.size() << std::endl;
}
if (!sqliteTableHasRows(db, "pending_delete")) {
std::vector<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(?)";
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);
sqlite3_step(stmt);
}
}
sqlite3_finalize(stmt);
std::cout << "[SQLITE] Imported pending deletes: " << pending.size() << std::endl;
}
_mkdir(FACES_DIR.c_str());
sqlite3_close(db);
std::cout << "[SQLITE] Storage ready: " << SQLITE_DB_FILE << std::endl;
return true;
}
// ============= 数据库操作 =============
std::string readDatabase(int limit = -1, int offset = 0) {
sqlite3* db = openSqliteDb();
if (!db) return "[]";
std::ostringstream json;
json << "[";
sqlite3_stmt* stmt = nullptr;
std::string sql = "SELECT timestamp,student_id,name,confidence,status,class_id FROM records ORDER BY id DESC";
if (limit > 0) {
sql += " LIMIT ? OFFSET ?";
}
bool first = true;
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) {
if (limit > 0) {
sqlite3_bind_int(stmt, 1, limit);
sqlite3_bind_int(stmt, 2, offset);
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
if (!first) json << ",";
first = false;
std::string conf = sqliteColumnText(stmt, 3);
if (conf.empty()) conf = "0";
std::string class_id = sqliteColumnText(stmt, 5);
if (class_id.empty()) class_id = "0";
json << "{"
<< "\"timestamp\":\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\","
<< "\"student_id\":\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\","
<< "\"name\":\"" << jsonEscape(sqliteColumnText(stmt, 2)) << "\","
<< "\"confidence\":" << conf << ","
<< "\"status\":\"" << jsonEscape(sqliteColumnText(stmt, 4)) << "\","
<< "\"class_id\":\"" << jsonEscape(class_id) << "\""
<< "}";
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
json << "]";
return json.str();
}
std::string getStatistics() {
sqlite3* db = openSqliteDb();
if (!db) return "{\"total\":0,\"today\":0}";
int total = 0;
int today = 0;
std::string todayDate = getCurrentTimestamp().substr(0, 10);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM records", -1, &stmt, nullptr) == SQLITE_OK
&& sqlite3_step(stmt) == SQLITE_ROW) {
total = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
stmt = nullptr;
if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM records WHERE substr(timestamp,1,10)=?", -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, todayDate);
if (sqlite3_step(stmt) == SQLITE_ROW) {
today = sqlite3_column_int(stmt, 0);
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
std::ostringstream json;
json << "{"
<< "\"total\":" << total << ","
<< "\"today\":" << today << ","
<< "\"server_time\":\"" << getCurrentTimestamp() << "\""
<< "}";
return json.str();
}
bool clearDatabase() {
sqlite3* db = openSqliteDb();
if (!db) return false;
bool ok = sqliteExec(db, "DELETE FROM records");
sqlite3_close(db);
return ok;
}
// ============= 时间段配置管理 =============
bool saveTimeSlots(const std::string& json_data) {
std::cout << "[TIMESLOTS] Attempting to save " << json_data.size() << " bytes" << std::endl;
if (json_data.empty()) {
std::cerr << "[TIMESLOTS] ERROR: json_data is empty!" << std::endl;
return false;
}
if (!sqliteSetKvValue("time_slots", json_data)) {
std::cerr << "[TIMESLOTS] Failed to save configuration to SQLite" << std::endl;
return false;
}
std::cout << "[TIMESLOTS] ✓ Configuration saved successfully to SQLite" << std::endl;
std::cout << "[TIMESLOTS] Preview: " << json_data.substr(0, 100) << "..." << std::endl;
return true;
}
std::string readTimeSlots() {
std::string content = sqliteGetKvValue("time_slots", DEFAULT_TIMESLOTS_JSON);
std::cout << "[TIMESLOTS] Read " << content.size() << " bytes from SQLite" << std::endl;
return content;
}
bool saveMappingConfig(const std::string& json_data) {
if (json_data.empty()) return false;
return sqliteSetKvValue("mapping_config", json_data);
}
std::string readMappingConfig() {
return sqliteGetKvValue("mapping_config", "{\"student_name_map\":{},\"class_name_map\":{}}");
}
unsigned long long readSyncVersion() {
unsigned long long version = 1;
std::string value = sqliteGetKvValue("sync_version", "1");
std::istringstream ss(value);
if (!(ss >> version)) {
version = 1;
}
if (version == 0) version = 1;
return version;
}
unsigned long long bumpSyncVersion(const std::string& reason) {
unsigned long long version = readSyncVersion() + 1;
sqliteSetKvValue("sync_version", std::to_string(version));
std::cout << "[SYNC] Version bumped to " << version << " (" << reason << ")" << std::endl;
return version;
}
unsigned long long parseVersionParam(const std::map<std::string, std::string>& 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<unsigned char>(c))) return 0;
value = value * 10 + static_cast<unsigned long long>(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<unsigned char>(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<std::string> extractJsonStringArray(const std::string& json, const std::string& key) {
std::vector<std::string> 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;
}
std::string makeSyncStateResponse(const std::string& client, unsigned long long last_version) {
const unsigned long long version = readSyncVersion();
const bool changed = last_version < version;
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 = extractJsonMemberValue(getPendingDeleteList(), "pending_delete", "[]");
json << "\"face_mappings\":" << mappings
<< ",\"pending_delete\":" << pending;
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 std::vector<std::string> deleted = extractJsonStringArray(request_body, "deleted");
int confirmed_delete_count = 0;
for (const std::string& id : deleted) {
if (!id.empty() && removeFromPendingDelete(id)) {
confirmed_delete_count++;
}
}
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() << ","
<< "\"server_time\":\"" << jsonEscape(getCurrentTimestamp()) << "\""
<< "}";
return json.str();
}
std::string makeHealthResponse() {
std::string quick_check_message;
const bool quick_ok = sqliteQuickCheck(quick_check_message);
std::ostringstream json;
json << "{"
<< "\"status\":\"" << (quick_ok ? "healthy" : "degraded") << "\","
<< "\"time\":\"" << jsonEscape(getCurrentTimestamp()) << "\","
<< "\"storage\":\"sqlite\","
<< "\"database\":\"" << jsonEscape(SQLITE_DB_FILE) << "\","
<< "\"database_ok\":" << (quick_ok ? "true" : "false") << ","
<< "\"quick_check\":\"" << jsonEscape(quick_check_message) << "\","
<< "\"sync_version\":" << readSyncVersion()
<< "}";
return json.str();
}
// ============= 检查重复签到 =============
// 参数: student_id - 学号, slot_start - 时段开始时间(HH:MM), slot_end - 时段结束时间(HH:MM)
// 返回: JSON格式 {"duplicate": true/false, "record_time": "..."}
std::string checkDuplicate(const std::string& student_id, const std::string& slot_start, const std::string& slot_end) {
std::string todayDate = getCurrentTimestamp().substr(0, 10);
int start_hour = 0, start_minute = 0, end_hour = 23, end_minute = 59;
parseTimeParts(slot_start, start_hour, start_minute);
parseTimeParts(slot_end, end_hour, end_minute);
int start_total_minutes = start_hour * 60 + start_minute;
int end_total_minutes = end_hour * 60 + end_minute;
sqlite3* db = openSqliteDb();
if (!db) return "{\"duplicate\":false}";
sqlite3_stmt* stmt = nullptr;
const char* sql = "SELECT timestamp FROM records WHERE student_id=? AND substr(timestamp,1,10)=? ORDER BY id DESC";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, student_id);
sqliteBindText(stmt, 2, todayDate);
while (sqlite3_step(stmt) == SQLITE_ROW) {
std::string timestamp = sqliteColumnText(stmt, 0);
if (timestamp.length() < 16) continue;
int record_hour = 0;
int record_minute = 0;
if (!parseIntInRange(timestamp.substr(11, 2), 0, 23, record_hour)
|| !parseIntInRange(timestamp.substr(14, 2), 0, 59, record_minute)) continue;
int record_total_minutes = record_hour * 60 + record_minute;
if (record_total_minutes >= start_total_minutes && record_total_minutes <= end_total_minutes) {
sqlite3_finalize(stmt);
sqlite3_close(db);
std::cout << "[CHECK] Duplicate found: " << student_id << " at " << timestamp << std::endl;
return "{\"duplicate\":true,\"record_time\":\"" + jsonEscape(timestamp) + "\"}";
}
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
return "{\"duplicate\":false}";
}
// ============= 删除记录 =============
bool deleteRecord(const std::string& timestamp, const std::string& student_id) {
sqlite3* db = openSqliteDb();
if (!db) return false;
sqlite3_stmt* stmt = nullptr;
const char* sql = "DELETE FROM records WHERE timestamp=? AND student_id=?";
bool ok = false;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, timestamp);
sqliteBindText(stmt, 2, student_id);
ok = sqlite3_step(stmt) == SQLITE_DONE && sqlite3_changes(db) > 0;
}
sqlite3_finalize(stmt);
sqlite3_close(db);
return ok;
}
// ============= 人脸照片管理函数 =============
// 保存人脸ID映射配置
bool saveFaceIdMapping(const std::string& json_data) {
std::cout << "[FACE_MAPPING] Saving " << json_data.size() << " bytes" << std::endl;
std::map<std::string, std::string> mappings = parseFaceIdMappings(json_data);
sqlite3* db = openSqliteDb();
if (!db) return false;
bool ok = sqliteExec(db, "BEGIN") && sqliteExec(db, "DELETE FROM face_mappings");
sqlite3_stmt* stmt = nullptr;
const char* sql = "REPLACE INTO face_mappings(face_id,student_id) VALUES(?,?)";
if (ok && sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
for (const auto& m : mappings) {
sqlite3_reset(stmt);
sqlite3_clear_bindings(stmt);
sqliteBindText(stmt, 1, m.first);
sqliteBindText(stmt, 2, m.second);
if (sqlite3_step(stmt) != SQLITE_DONE) {
ok = false;
break;
}
}
} else {
ok = false;
}
sqlite3_finalize(stmt);
sqliteExec(db, ok ? "COMMIT" : "ROLLBACK");
sqlite3_close(db);
return ok;
}
std::map<std::string, std::string> parseFaceIdMappings(const std::string& json_data) {
// 解析JSON: {"mappings":{"1":"20210001","2":"20210002"}}
std::map<std::string, std::string> mappings;
size_t mappings_start = json_data.find("\"mappings\":{");
if (mappings_start == std::string::npos) return mappings;
mappings_start += 12; // 跳过 "mappings":{
size_t mappings_end = json_data.find("}", mappings_start);
if (mappings_end == std::string::npos) return mappings;
std::string mappings_str = json_data.substr(mappings_start, mappings_end - mappings_start);
// 解析每个映射对
size_t pos = 0;
while (pos < mappings_str.length()) {
// 找到键
size_t key_start = mappings_str.find("\"", pos);
if (key_start == std::string::npos) break;
size_t key_end = mappings_str.find("\"", key_start + 1);
if (key_end == std::string::npos) break;
// 找到值
size_t val_start = mappings_str.find("\"", key_end + 1);
if (val_start == std::string::npos) break;
size_t val_end = mappings_str.find("\"", val_start + 1);
if (val_end == std::string::npos) break;
std::string key = mappings_str.substr(key_start + 1, key_end - key_start - 1);
std::string value = mappings_str.substr(val_start + 1, val_end - val_start - 1);
if (!key.empty() && !value.empty()) {
mappings[key] = value;
}
pos = val_end + 1;
}
return mappings;
}
std::map<std::string, std::string> loadFaceIdMappings() {
std::map<std::string, std::string> 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<std::string> resolveK210FaceIdsForDelete(const std::string& requested_id) {
std::set<std::string> 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<std::string> resolveStudentIdsForDelete(const std::string& requested_id) {
std::set<std::string> ids;
ids.insert(requested_id);
const auto mappings = loadFaceIdMappings();
for (const auto& m : mappings) {
if (m.first == requested_id || m.second == requested_id) {
ids.insert(m.second);
}
}
return ids;
}
// 根据映射重命名照片文件
void renameFacePhotos(const std::string& json_data) {
std::cout << "[FACE_PHOTO] Renaming photos based on mapping..." << std::endl;
const std::map<std::string, std::string> mappings = parseFaceIdMappings(json_data);
// 遍历faces目录,重命名匹配的照片
std::vector<std::pair<std::string, std::string>> rename_list;
#ifdef _WIN32
WIN32_FIND_DATAA findData;
std::string searchPath = FACES_DIR + "\\*.jpg";
HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData);
if (hFind != INVALID_HANDLE_VALUE) {
do {
std::string filename = findData.cFileName;
std::string base_name = filename.substr(0, filename.length() - 4); // 去掉.jpg
// 检查是否以某个face_id开头
for (const auto& m : mappings) {
// 匹配 "face_id.jpg" 或 "face_id_xxx.jpg"
if (base_name == m.first ||
(base_name.find(m.first + "_") == 0)) {
// 直接使用学号作为新文件名(不保留时间戳)
std::string new_filename = m.second + ".jpg";
if (filename != new_filename) {
rename_list.push_back({filename, new_filename});
}
break;
}
}
} while (FindNextFileA(hFind, &findData));
FindClose(hFind);
}
#else
DIR* dir = opendir(FACES_DIR.c_str());
if (dir) {
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
std::string filename = entry->d_name;
if (filename.size() > 4 && filename.substr(filename.size() - 4) == ".jpg") {
std::string base_name = filename.substr(0, filename.length() - 4);
for (const auto& m : mappings) {
if (base_name == m.first ||
(base_name.find(m.first + "_") == 0)) {
std::string new_filename = m.second + ".jpg";
if (filename != new_filename) {
rename_list.push_back({filename, new_filename});
}
break;
}
}
}
}
closedir(dir);
}
#endif
// 执行重命名
for (const auto& r : rename_list) {
std::string old_id = r.first.substr(0, r.first.size() - 4);
std::string new_id = r.second.substr(0, r.second.size() - 4);
std::string old_path;
std::string new_path;
if (!buildFacePhotoPath(old_id, old_path) || !buildFacePhotoPath(new_id, new_path)) {
std::cerr << "[FACE_PHOTO] Skip unsafe rename: " << r.first << " -> " << r.second << std::endl;
continue;
}
std::remove(new_path.c_str());
if (std::rename(old_path.c_str(), new_path.c_str()) == 0) {
std::cout << "[FACE_PHOTO] Renamed: " << r.first << " -> " << r.second << std::endl;
} else {
std::cerr << "[FACE_PHOTO] Rename failed: " << r.first << std::endl;
}
}
std::cout << "[FACE_PHOTO] Renamed " << rename_list.size() << " photos" << std::endl;
}
// 读取人脸ID映射配置
std::string readFaceIdMapping() {
std::map<std::string, std::string> mappings = loadFaceIdMappings();
std::ostringstream json;
json << "{\"mappings\":{";
bool first = true;
for (const auto& m : mappings) {
if (!first) json << ",";
json << "\"" << jsonEscape(m.first) << "\":\"" << jsonEscape(m.second) << "\"";
first = false;
}
json << "}}";
return json.str();
}
// 获取人脸照片列表
std::string getFacePhotosList() {
std::ostringstream json;
json << "{\"photos\":[";
std::vector<std::string> photos;
#ifdef _WIN32
WIN32_FIND_DATAA findData;
std::string searchPath = FACES_DIR + "\\*.jpg";
HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData);
if (hFind != INVALID_HANDLE_VALUE) {
do {
photos.push_back(findData.cFileName);
} while (FindNextFileA(hFind, &findData));
FindClose(hFind);
}
#else
DIR* dir = opendir(FACES_DIR.c_str());
if (dir) {
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
std::string name = entry->d_name;
if (name.size() > 4 && name.substr(name.size() - 4) == ".jpg") {
photos.push_back(name);
}
}
closedir(dir);
}
#endif
std::sort(photos.begin(), photos.end());
for (size_t i = 0; i < photos.size(); ++i) {
if (i > 0) json << ",";
std::string id = photos[i].substr(0, photos[i].size() - 4);
json << "{\"id\":\"" << jsonEscape(id) << "\",\"filename\":\"" << jsonEscape(photos[i]) << "\"}";
}
json << "]}";
return json.str();
}
// 保存人脸照片
bool saveFacePhoto(const std::string& face_id, const std::string& photo_data) {
_mkdir(FACES_DIR.c_str());
std::string filepath;
if (!buildFacePhotoPath(face_id, filepath)) {
std::cerr << "[FACE_PHOTO] Invalid face_id: " << face_id << std::endl;
return false;
}
std::ofstream file(filepath, std::ios::binary);
if (!file.is_open()) {
std::cerr << "[FACE_PHOTO] Failed to create: " << filepath << std::endl;
return false;
}
file.write(photo_data.c_str(), photo_data.size());
file.close();
std::cout << "[FACE_PHOTO] Saved: " << filepath << " (" << photo_data.size() << " bytes)" << std::endl;
return true;
}
// 读取人脸照片
std::string readFacePhoto(const std::string& face_id) {
std::string filepath;
if (!buildFacePhotoPath(face_id, filepath)) return "";
std::ifstream file(filepath, std::ios::binary);
if (!file.is_open()) return "";
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
return content;
}
// 删除人脸照片
bool deleteFacePhoto(const std::string& face_id) {
std::string filepath;
if (!buildFacePhotoPath(face_id, filepath)) return false;
if (std::remove(filepath.c_str()) == 0) {
std::cout << "[FACE_PHOTO] Deleted: " << filepath << std::endl;
return true;
}
return false;
}
// 从人脸ID映射中删除指定ID
bool removeFaceIdFromMapping(const std::string& face_id) {
sqlite3* db = openSqliteDb();
if (!db) return false;
sqlite3_stmt* stmt = nullptr;
bool ok = false;
if (sqlite3_prepare_v2(db, "DELETE FROM face_mappings WHERE face_id=?", -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, face_id);
ok = sqlite3_step(stmt) == SQLITE_DONE && sqlite3_changes(db) > 0;
}
sqlite3_finalize(stmt);
sqlite3_close(db);
if (ok) std::cout << "[FACE_MAPPING] Removed face_id: " << face_id << std::endl;
return ok;
}
// 删除指定学生ID的所有考勤记录
int deleteRecordsByStudentId(const std::string& student_id) {
sqlite3* db = openSqliteDb();
if (!db) return 0;
sqlite3_stmt* stmt = nullptr;
int deleted_count = 0;
if (sqlite3_prepare_v2(db, "DELETE FROM records WHERE student_id=?", -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, student_id);
if (sqlite3_step(stmt) == SQLITE_DONE) {
deleted_count = sqlite3_changes(db);
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
return deleted_count;
}
// 获取所有学生ID(从考勤记录提取,包括没有照片的)
std::string getAllStudentIds() {
std::set<std::string> student_ids;
sqlite3* db = openSqliteDb();
if (db) {
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, "SELECT DISTINCT student_id FROM records WHERE student_id<>'' ORDER BY student_id", -1, &stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
student_ids.insert(sqliteColumnText(stmt, 0));
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
}
// 构建JSON响应
std::ostringstream json;
json << "{\"student_ids\":[";
bool first = true;
for (const auto& id : student_ids) {
if (!first) json << ",";
json << "\"" << jsonEscape(id) << "\"";
first = false;
}
json << "]}";
return json.str();
}
// 添加人脸ID到待删除列表
bool addToPendingDelete(const std::string& face_id) {
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);
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 getPendingDeleteList() {
sqlite3* db = openSqliteDb();
if (!db) return "{\"pending_delete\":[]}";
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) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
if (!first) json << ",";
json << "\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\"";
first = false;
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
json << "]}";
return json.str();
}
// 从待删除列表中移除指定ID(K210确认删除后调用)
bool removeFromPendingDelete(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 pending_delete WHERE face_id=?", -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, face_id);
ok = sqlite3_step(stmt) == SQLITE_DONE && sqlite3_changes(db) > 0;
}
sqlite3_finalize(stmt);
sqlite3_close(db);
std::cout << "[PENDING_DELETE] Removed: " << face_id << std::endl;
return ok;
}
// ============= K210同步请求管理 =============
// 设置同步请求标志(QT端调用)
bool setSyncRequest(bool sync) {
if (!sqliteSetKvValue("sync_request", std::string("{\"sync\":") + (sync ? "true" : "false") + "}")) {
return false;
}
std::cout << "[SYNC] Sync request set to: " << (sync ? "true" : "false") << std::endl;
return true;
}
// 获取同步请求状态(K210轮询调用)
std::string getSyncRequest() {
return sqliteGetKvValue("sync_request", "{\"sync\":false}");
}
// 清除同步请求标志(K210确认后调用)
bool clearSyncRequest() {
return setSyncRequest(false);
}
// 根据K210的face_id查询真实学号
std::string getMappedStudentId(const std::string& face_id) {
std::cout << "[MAPPING] Looking up face_id: " << face_id << std::endl;
const auto mappings = loadFaceIdMappings();
auto it = mappings.find(face_id);
if (it != mappings.end() && !it->second.empty()) {
std::cout << "[MAPPING] ✓ Face ID " << face_id << " -> Student ID " << it->second << std::endl;
return it->second;
}
std::cout << "[MAPPING] No mapping found for face_id: " << face_id << std::endl;
return face_id; // 没找到映射,返回原ID
}
// ============= HTTP请求处理 =============
void handleHttpRequest(SOCKET client_socket) {
char buffer[BUFFER_SIZE] = {0};
int bytes = recv(client_socket, buffer, BUFFER_SIZE - 1, 0);
if (bytes <= 0) {
closesocket(client_socket);
return;
}
std::string request(buffer, bytes);
std::istringstream stream(request);
std::string method, path, version;
stream >> method >> path >> version;
std::cout << "[HTTP] " << method << " " << path << std::endl;
std::string response_body;
std::string content_type = "application/json";
int status_code = 200;
std::string status_text = "OK";
auto sendResponse = [&]() {
std::ostringstream response;
response << "HTTP/1.1 " << status_code << " " << status_text << "\r\n";
response << "Content-Type: " << content_type << "; charset=utf-8\r\n";
response << "Content-Length: " << response_body.length() << "\r\n";
response << "Access-Control-Allow-Origin: " << allowedCorsOrigin() << "\r\n";
response << "Access-Control-Allow-Headers: Content-Type, X-Api-Token, X-Encrypted-Response, X-Encrypted\r\n";
response << "Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS\r\n";
response << "Connection: close\r\n";
response << "\r\n";
response << response_body;
std::string resp_str = response.str();
send(client_socket, resp_str.c_str(), resp_str.length(), 0);
closesocket(client_socket);
};
std::string endpoint = path;
std::string query;
size_t q_pos = path.find('?');
if (q_pos != std::string::npos) {
endpoint = path.substr(0, q_pos);
query = path.substr(q_pos + 1);
}
const std::string original_endpoint = endpoint;
endpoint = normalizeApiEndpoint(method, endpoint);
if (endpoint != original_endpoint) {
std::cout << "[HTTP] Route alias: " << original_endpoint << " -> " << endpoint << std::endl;
}
if (!isAuthorizedRequest(request, method, endpoint)) {
response_body = makeJsonError("Unauthorized");
status_code = 401;
status_text = "Unauthorized";
sendResponse();
return;
}
// 提取请求体
std::string request_body;
if (method == "POST") {
size_t content_length = 0;
std::string cl_str;
if (getHeaderValue(request, "Content-Length", cl_str)) {
int parsed_content_length = 0;
if (!parseIntInRange(cl_str, 0, static_cast<int>(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<size_t>(parsed_content_length);
}
const bool is_photo_upload = endpoint.find("/api/face_photo") == 0;
const size_t max_body_size = is_photo_upload ? MAX_PHOTO_BODY_SIZE : MAX_JSON_BODY_SIZE;
if (content_length > max_body_size) {
response_body = makeJsonError("Request body too large");
status_code = 413;
status_text = "Payload Too Large";
sendResponse();
return;
}
size_t header_end = request.find("\r\n\r\n");
if (header_end != std::string::npos) {
size_t body_start = header_end + 4;
size_t current_body_size = request.size() - body_start;
if (current_body_size >= content_length) {
request_body = request.substr(body_start, content_length);
} else {
request_body = request.substr(body_start);
size_t remaining = content_length - current_body_size;
while (remaining > 0) {
char extra_buffer[BUFFER_SIZE] = {0};
int extra_bytes = recv(client_socket, extra_buffer,
static_cast<int>(std::min(remaining, (size_t)BUFFER_SIZE)), 0);
if (extra_bytes <= 0) break;
request_body.append(extra_buffer, extra_bytes);
remaining -= extra_bytes;
}
}
}
std::cout << "[HTTP] POST body size: " << request_body.size()
<< " bytes (expected: " << content_length << ")" << std::endl;
if (request_body.size() != content_length) {
response_body = makeJsonError("Incomplete request body");
status_code = 400;
status_text = "Bad Request";
sendResponse();
return;
}
}
// ============= 路由处理 =============
if (method == "OPTIONS") {
response_body = "{}";
} else if (method == "GET" && endpoint == "/api/v1/sync/version") {
response_body = makeSyncVersionResponse();
std::cout << "[SYNC] Version fetched: " << readSyncVersion() << std::endl;
} else if (method == "GET" && (endpoint == "/api/sync/state" || endpoint == "/api/v1/sync/state")) {
auto params = parseQueryParams(query);
const std::string client = params.count("client") ? params["client"] : "all";
const unsigned long long last_version = parseVersionParam(params);
response_body = makeSyncStateResponse(client, last_version);
std::cout << "[SYNC] State fetched: client=" << client
<< ", last_version=" << last_version
<< ", current_version=" << readSyncVersion() << std::endl;
} else if (method == "POST" && (endpoint == "/api/sync/ack" || endpoint == "/api/v1/sync/ack")) {
response_body = makeSyncAckResponse(request_body);
std::cout << "[SYNC] Ack received: " << request_body << std::endl;
// 新增:处理加密签到数据
} else if (method == "POST" && endpoint == "/api/checkin") {
std::cout << "[CHECKIN] Processing encrypted checkin data..." << std::endl;
// 检查是否是加密数据
if (request.find("X-Encrypted: AES128-CBC") != std::string::npos) {
std::cout << "[CHECKIN] Encrypted data detected" << std::endl;
// 解密数据
std::string decrypted_json = decryptJSON(request_body);
if (decrypted_json.empty()) {
response_body = "{\"success\":false,\"message\":\"解密失败\"}";
status_code = 400;
status_text = "Bad Request";
} else {
// 解析JSON
auto json_data = parseJSON(decrypted_json);
if (json_data.find("student_id") != json_data.end()) {
// 构建考勤记录
std::string timestamp = getCurrentTimestamp();
std::string student_id = json_data["student_id"]; // K210已发送真实学号
std::string name = json_data["name"];
std::string confidence = json_data["confidence"];
std::string status = json_data["status"];
std::string class_id = json_data.count("class_id") ? json_data["class_id"] : "0";
std::cout << "[CHECKIN] Student ID: " << student_id << std::endl;
// 保存到SQLite(包含班级编号)
if (saveAttendanceRecord(timestamp, student_id, name, confidence, status, class_id)) {
response_body = "{\"success\":true,\"message\":\"签到成功\",\"timestamp\":\""
+ jsonEscape(timestamp) + "\"}";
bumpSyncVersion("checkin");
std::cout << "[CHECKIN] ✓ " << name << " (ID:" << student_id << ", Class:" << class_id << ")" << std::endl;
} else {
response_body = "{\"success\":false,\"message\":\"数据库写入失败\"}";
status_code = 500;
status_text = "Internal Server Error";
}
} else {
response_body = "{\"success\":false,\"message\":\"缺少必要字段\"}";
status_code = 400;
status_text = "Bad Request";
}
}
} else {
response_body = "{\"success\":false,\"message\":\"未加密数据\"}";
status_code = 400;
status_text = "Bad Request";
}
}
// API端点:返回加密数据
else if (method == "GET" && endpoint == "/api/records") {
auto params = parseQueryParams(query);
int limit = 100;
int offset = 0;
if (params.count("limit") && !parseIntInRange(params["limit"], 1, 5000, limit)) {
response_body = makeJsonError("Invalid limit");
status_code = 400;
status_text = "Bad Request";
} else if (params.count("offset") && !parseIntInRange(params["offset"], 0, 1000000, offset)) {
response_body = makeJsonError("Invalid offset");
status_code = 400;
status_text = "Bad Request";
} else {
std::string json_data = readDatabase(limit, offset);
if (hasHeaderToken(request, "X-Encrypted-Response", "true")) {
response_body = encryptJSON(json_data);
if (response_body.empty()) {
response_body = makeJsonError("Encryption failed");
status_code = 500;
status_text = "Internal Server Error";
}
} else {
response_body = json_data;
}
}
} else if (method == "GET" && endpoint == "/api/statistics") {
std::string json_data = getStatistics();
if (hasHeaderToken(request, "X-Encrypted-Response", "true")) {
response_body = encryptJSON(json_data);
if (response_body.empty()) {
response_body = makeJsonError("Encryption failed");
status_code = 500;
status_text = "Internal Server Error";
}
} else {
response_body = json_data;
}
} else if (method == "POST" && endpoint == "/api/clear") {
if (clearDatabase()) {
response_body = "{\"success\":true,\"message\":\"Database cleared\"}";
bumpSyncVersion("clear_records");
} else {
response_body = "{\"success\":false,\"message\":\"Failed to clear database\"}";
status_code = 500;
status_text = "Internal Server Error";
}
} else if (method == "DELETE" && endpoint == "/api/record") {
auto params = parseQueryParams(query);
if (params.count("timestamp") && params.count("student_id")) {
std::string timestamp = params["timestamp"];
std::string student_id = params["student_id"];
if (deleteRecord(timestamp, student_id)) {
response_body = "{\"success\":true,\"message\":\"Record deleted\"}";
bumpSyncVersion("delete_record");
} else {
response_body = "{\"success\":false,\"message\":\"Record not found\"}";
status_code = 404;
status_text = "Not Found";
}
} else {
response_body = "{\"success\":false,\"message\":\"Missing parameters\"}";
status_code = 400;
status_text = "Bad Request";
}
} else if (method == "GET" && endpoint == "/api/health") {
response_body = makeHealthResponse();
} else if (method == "GET" && endpoint == "/api/v1/system/storage") {
response_body = makeStorageStatusResponse();
} else if (method == "GET" && endpoint == "/api/time") {
// 返回服务器当前Unix时间戳(UTC秒)和可读时间
std::ostringstream json;
json << "{"
<< "\"epoch\":" << static_cast<long long>(time(0)) << ","
<< "\"server_time\":\"" << getCurrentTimestamp() << "\""
<< "}";
response_body = json.str();
} else if (method == "POST" && endpoint == "/api/timeslots") {
// 上传时间段配置: POST /api/timeslots
std::cout << "[TIMESLOTS] Received upload request, data size: " << request_body.size() << " bytes" << std::endl;
if (saveTimeSlots(request_body)) {
response_body = "{\"success\":true,\"message\":\"时间段配置已更新\"}";
bumpSyncVersion("timeslots");
std::cout << "[TIMESLOTS] ✓ Configuration uploaded successfully" << std::endl;
} else {
response_body = "{\"success\":false,\"message\":\"保存时间段配置失败\"}";
status_code = 500;
status_text = "Internal Server Error";
}
} else if (method == "GET" && endpoint == "/api/timeslots") {
// 获取时间段配置: GET /api/timeslots
std::cout << "[TIMESLOTS] Reading configuration" << std::endl;
response_body = readTimeSlots();
} else if (method == "GET" && endpoint == "/api/check_duplicate") {
// 检查重复签到: GET /api/check_duplicate?student_id=xxx&slot_start=08:00&slot_end=09:30
auto params = parseQueryParams(query);
if (params.count("student_id") && params.count("slot_start") && params.count("slot_end")) {
response_body = checkDuplicate(params["student_id"], params["slot_start"], params["slot_end"]);
std::cout << "[CHECK] student_id=" << params["student_id"]
<< ", slot=" << params["slot_start"] << "-" << params["slot_end"]
<< ", result=" << response_body << std::endl;
} else {
response_body = "{\"error\":\"Missing parameters: student_id, slot_start, slot_end\"}";
status_code = 400;
status_text = "Bad Request";
}
} else if (method == "GET" && endpoint == "/api/today_records") {
// 获取今日已签到记录(供STM32开机下载)
// GET /api/today_records?slot_start=08:00&slot_end=09:30
auto params = parseQueryParams(query);
std::string slot_start = params.count("slot_start") ? params["slot_start"] : "00:00";
std::string slot_end = params.count("slot_end") ? params["slot_end"] : "23:59";
std::string todayDate = getCurrentTimestamp().substr(0, 10);
int start_hour = 0, start_minute = 0, end_hour = 23, end_minute = 59;
parseTimeParts(slot_start, start_hour, start_minute);
parseTimeParts(slot_end, end_hour, end_minute);
std::ostringstream json;
json << "{\"date\":\"" << jsonEscape(todayDate) << "\",\"records\":[";
sqlite3* db = openSqliteDb();
if (db) {
sqlite3_stmt* stmt = nullptr;
bool first = true;
const char* sql = "SELECT timestamp,student_id FROM records WHERE substr(timestamp,1,10)=? ORDER BY id ASC";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, todayDate);
while (sqlite3_step(stmt) == SQLITE_ROW) {
std::string timestamp = sqliteColumnText(stmt, 0);
if (timestamp.length() < 16) continue;
int record_hour = 0;
int record_minute = 0;
if (!parseIntInRange(timestamp.substr(11, 2), 0, 23, record_hour)
|| !parseIntInRange(timestamp.substr(14, 2), 0, 59, record_minute)) continue;
int record_total = record_hour * 60 + record_minute;
int start_total = start_hour * 60 + start_minute;
int end_total = end_hour * 60 + end_minute;
if (record_total < start_total || record_total > end_total) continue;
if (!first) json << ",";
json << "\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\"";
first = false;
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
}
json << "]}";
response_body = json.str();
std::cout << "[TODAY] Returning today's records: " << response_body << std::endl;
} else if (method == "POST" && endpoint == "/api/mapping_config") {
// 上传映射配置: POST /api/mapping_config
std::cout << "[MAPPING] Received upload request, data size: " << request_body.size() << " bytes" << std::endl;
if (saveMappingConfig(request_body)) {
response_body = "{\"success\":true,\"message\":\"映射配置已保存\"}";
bumpSyncVersion("mapping_config");
std::cout << "[MAPPING] ✓ Configuration saved successfully" << std::endl;
} else {
response_body = "{\"success\":false,\"message\":\"保存映射配置失败\"}";
status_code = 500;
status_text = "Internal Server Error";
}
} else if (method == "GET" && endpoint == "/api/mapping_config") {
// 获取映射配置: GET /api/mapping_config
std::cout << "[MAPPING] Reading configuration" << std::endl;
response_body = readMappingConfig();
std::cout << "[MAPPING] Returning config: " << response_body.size() << " bytes" << std::endl;
// ============= 人脸照片管理 API =============
} else if (method == "GET" && endpoint == "/api/face_photos") {
// 获取人脸照片列表
_mkdir(FACES_DIR.c_str());
response_body = getFacePhotosList();
std::cout << "[FACE_PHOTO] Photos list fetched" << std::endl;
} else if (method == "GET" && endpoint.find("/api/face_photo/") == 0) {
// 获取指定人脸照片: GET /api/face_photo/<id>
std::string face_id = urlDecode(endpoint.substr(16));
std::string photo_data = readFacePhoto(face_id);
if (!photo_data.empty()) {
response_body = photo_data;
content_type = "image/jpeg";
std::cout << "[FACE_PHOTO] Photo fetched for id: " << face_id << std::endl;
} else {
response_body = "{\"error\":\"Photo not found\"}";
status_code = 404;
status_text = "Not Found";
}
} else if (method == "POST" && endpoint.find("/api/face_photo_simple/") == 0) {
// 简化版人脸照片上传: POST /api/face_photo_simple/<face_id>
std::string face_id = urlDecode(endpoint.substr(23));
std::cout << "[FACE_PHOTO] Simple upload for face_id: " << face_id << ", size: " << request_body.size() << " bytes" << std::endl;
if (!isSafeId(face_id)) {
response_body = "{\"success\":false,\"message\":\"Invalid face_id\"}";
status_code = 400;
status_text = "Bad Request";
} else if (!request_body.empty()) {
if (saveFacePhoto(face_id, request_body)) {
response_body = "{\"success\":true,\"message\":\"Photo saved\",\"face_id\":\"" + jsonEscape(face_id) + "\"}";
bumpSyncVersion("face_photo");
} else {
response_body = "{\"success\":false,\"message\":\"Failed to save photo\"}";
status_code = 500;
status_text = "Internal Server Error";
}
} else {
response_body = "{\"success\":false,\"message\":\"Invalid face_id or empty data\"}";
status_code = 400;
status_text = "Bad Request";
}
} else if (method == "POST" && endpoint == "/api/face_id_mapping") {
// 保存人脸ID映射
std::cout << "[FACE_MAPPING] Saving mapping, size: " << request_body.size() << " bytes" << std::endl;
if (saveFaceIdMapping(request_body)) {
// 根据新映射重命名照片文件
renameFacePhotos(request_body);
// 自动触发K210同步
bumpSyncVersion("face_id_mapping");
setSyncRequest(true);
std::cout << "[FACE_MAPPING] Auto-triggered K210 sync" << std::endl;
response_body = "{\"success\":true,\"message\":\"Face ID mapping saved, K210 sync triggered\"}";
} else {
response_body = "{\"success\":false,\"message\":\"Failed to save mapping\"}";
status_code = 500;
status_text = "Internal Server Error";
}
} else if (method == "GET" && endpoint == "/api/face_id_mapping") {
// 获取人脸ID映射
response_body = readFaceIdMapping();
std::cout << "[FACE_MAPPING] Mapping fetched" << std::endl;
} else if (method == "GET" && endpoint == "/api/all_student_ids") {
// 获取所有学生ID(从考勤记录提取)
response_body = getAllStudentIds();
std::cout << "[STUDENT_IDS] All student IDs fetched" << std::endl;
} else if (method == "DELETE" && endpoint.find("/api/face/") == 0) {
// 删除人脸信息: DELETE /api/face/<student_id>?delete_records=true
std::string student_id = urlDecode(endpoint.substr(10)); // 去掉 "/api/face/"
auto params = parseQueryParams(query);
bool delete_records = (params.count("delete_records") && params["delete_records"] == "true");
std::cout << "[DELETE_FACE] Deleting face for student_id: " << student_id
<< ", delete_records: " << (delete_records ? "yes" : "no") << std::endl;
if (!isSafeId(student_id)) {
response_body = "{\"success\":false,\"message\":\"Invalid student_id\"}";
status_code = 400;
status_text = "Bad Request";
} else {
const std::set<std::string> k210_face_ids = resolveK210FaceIdsForDelete(student_id);
const std::set<std::string> 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) || pending_added;
}
for (const auto& id : student_ids) {
photo_deleted = deleteFacePhoto(id) || photo_deleted;
}
bumpSyncVersion("delete_face");
setSyncRequest(true);
std::ostringstream result;
result << "{\"success\":true,"
<< "\"photo_deleted\":" << (photo_deleted ? "true" : "false") << ","
<< "\"mapping_deleted\":" << (mapping_deleted ? "true" : "false") << ","
<< "\"records_deleted\":" << records_deleted << ","
<< "\"k210_pending\":" << (pending_added ? "true" : "false") << ","
<< "\"sync_requested\":true}";
response_body = result.str();
std::cout << "[DELETE_FACE] Result: " << response_body << std::endl;
}
} else if (method == "GET" && endpoint == "/api/pending_delete") {
// K210查询待删除列表
response_body = getPendingDeleteList();
std::cout << "[PENDING_DELETE] List fetched" << std::endl;
} else if (method == "POST" && endpoint == "/api/confirm_delete") {
// K210确认删除完成,从待删除列表移除
// 请求体格式: {"face_id":"xxx"}
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);
}
}
if (!face_id.empty() && removeFromPendingDelete(face_id)) {
response_body = "{\"success\":true,\"message\":\"Confirmed delete: " + jsonEscape(face_id) + "\"}";
bumpSyncVersion("confirm_delete");
std::cout << "[PENDING_DELETE] K210 confirmed delete: " << face_id << std::endl;
} else {
response_body = "{\"success\":false,\"message\":\"Invalid face_id or not in pending list\"}";
status_code = 400;
status_text = "Bad Request";
}
// ============= K210同步请求 API =============
} else if (method == "GET" && endpoint == "/api/sync_request") {
// K210查询是否需要同步
response_body = getSyncRequest();
std::cout << "[SYNC] K210 checking sync request: " << response_body << std::endl;
} else if (method == "POST" && endpoint == "/api/sync_ack") {
// K210确认收到同步请求,清除标志
if (clearSyncRequest()) {
response_body = "{\"success\":true,\"message\":\"Sync acknowledged\"}";
std::cout << "[SYNC] K210 acknowledged sync request" << std::endl;
} else {
response_body = "{\"success\":false,\"message\":\"Failed to clear sync request\"}";
status_code = 500;
status_text = "Internal Server Error";
}
} else if (method == "POST" && endpoint == "/api/trigger_sync") {
// QT端触发K210同步
if (setSyncRequest(true)) {
response_body = "{\"success\":true,\"message\":\"Sync request sent to K210\"}";
std::cout << "[SYNC] QT triggered K210 sync" << std::endl;
} else {
response_body = "{\"success\":false,\"message\":\"Failed to set sync request\"}";
status_code = 500;
status_text = "Internal Server Error";
}
} else {
response_body = "{\"error\":\"Not Found\"}";
status_code = 404;
status_text = "Not Found";
}
sendResponse();
}
// ============= HTTP服务器线程 =============
void httpServerThread() {
SOCKET server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == INVALID_SOCKET) {
std::cerr << "[HTTP] Failed to create socket" << std::endl;
return;
}
int opt = 1;
setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
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 port " << 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;
// 初始化SQLite,并首次导入旧CSV/JSON配置
if (!initializeSqliteStorage()) {
std::cerr << "[SQLITE] Failed to initialize storage" << std::endl;
return 1;
}
std::cout << "[SYNC] Current version: " << readSyncVersion() << std::endl;
// 启动HTTP服务器
std::thread http_thread(httpServerThread);
http_thread.join();
#ifdef _WIN32
WSACleanup();
#endif
return 0;
}