/* * 增强版考勤服务器 - 支持 AES 加密数据 * * 本文件是智慧课堂考勤系统的服务端主程序,采用“单文件 C++ 服务”的形式实现。 * 它不依赖 Web 框架,而是直接使用 socket 监听 HTTP 请求,并通过 SQLite 保存运行数据。 * * 主要职责: * 1. HTTP 端口 8082:接收 STM32、K210 和 Qt 管理端的 API 请求; * 2. AES-128-CBC 加密:管理类 JSON 请求和响应使用 AES + Base64 传输,避免敏感数据明文暴露; * 3. SQLite 持久化:统一保存考勤记录、学生档案、班级、时间段、人脸档案、照片索引和同步状态; * 4. 人脸照片管理:保存 K210 上传的人脸 JPEG,并维护 face_id 与照片文件的对应关系; * 5. 统一同步机制:通过 sync_version 将 K210、STM32、Qt 三端需要同步的数据组织到同一套版本流中。 * * 端侧分工: * - K210:负责人脸识别、照片上传、拉取人脸映射和远程删除任务; * - STM32:负责本地签到控制、重复签到检查、记录上传和外设提示; * - Qt 管理端:负责查看记录、维护学生/班级/时间段、绑定人脸和发起删除。 * * 数据一致性原则: * - 任何影响终端状态的操作都会调用 bumpSyncVersion() 推进版本号; * - K210 删除本地特征后必须 ACK 服务端 pending_delete 任务,服务端才认为删除闭环完成; * - 新照片上传后会自动创建或补齐 face_profile,避免“有照片无档案”的孤立状态。 * * 编译:g++ -o server_encrypted.exe server_encrypted.cpp -lws2_32 -lcrypto -lssl -lsqlite3 -std=c++11 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #include #include #pragma comment(lib, "ws2_32.lib") typedef int socklen_t; #else #include #include #include #include #include #include #define SOCKET int #define INVALID_SOCKET -1 #define SOCKET_ERROR -1 #define closesocket(s) close(s) #define _mkdir(d) mkdir(d, 0755) #endif // OpenSSL库用于AES解密和Base64 #include #include #include #include #include const int HTTP_PORT = 8082; // 统一 HTTP 服务端口,K210、STM32、Qt 均访问该端口 const int BUFFER_SIZE = 4096; // 单次 socket 接收缓冲区大小,请求体不足时会循环补读 const std::string SQLITE_DB_FILE = "attendance.db"; // SQLite 主数据库文件 const std::string LOG_FILE = "attendance.log"; // 预留日志文件名,当前主要使用控制台输出 const std::string FACES_DIR = "faces"; // 人脸照片目录,照片以 faces/.jpg 形式保存 const std::string DB_BACKUP_DIR = "backups"; // 启动前自动备份数据库的目录 // 请求体大小限制。 // 嵌入式终端资源有限,服务端也需要防止异常请求占用过多内存: // - JSON 管理数据通常很小,限制为 64KB; // - K210 上传的 JPEG 人脸照片限制为 512KB; // - 加密后会经过 Base64 编码,体积约增加 1/3,因此单独设置加密体上限。 const size_t MAX_JSON_BODY_SIZE = 64 * 1024; const size_t MAX_PHOTO_BODY_SIZE = 512 * 1024; const size_t MAX_ENCRYPTED_JSON_BODY_SIZE = ((MAX_JSON_BODY_SIZE + 16) * 4 / 3) + 32; const size_t MAX_ENCRYPTED_PHOTO_BODY_SIZE = ((MAX_PHOTO_BODY_SIZE + 16) * 4 / 3) + 32; // 默认令牌和 CORS 来源可以通过环境变量覆盖: // - ATTENDANCE_API_TOKEN:部署时替换为真实接口令牌; // - ATTENDANCE_CORS_ORIGIN:Qt/Web 管理端跨域访问来源; // - ATTENDANCE_BIND_HOST:监听地址,默认 0.0.0.0。 const char* DEFAULT_API_TOKEN = "atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV"; const char* DEFAULT_CORS_ORIGIN = "http://localhost"; const char* DEFAULT_TIMESLOTS_JSON = "{\"time_slots\":[]}"; // 函数作用:读取指定环境变量,未配置时返回默认值。 std::string getEnvOrDefault(const char* name, const char* fallback) { // 读取环境变量,若未设置则使用默认值。 // 这样同一份程序可以在本机调试和服务器部署之间切换配置,而不用改代码。 const char* value = std::getenv(name); if (value && value[0] != '\0') { return std::string(value); } return std::string(fallback); } // 函数作用:返回服务端用于鉴权的 API Token。 const std::string& apiToken() { // 使用 static 局部变量保证环境变量只读取一次。 // 运行期间令牌保持稳定,避免多线程请求中配置突然变化。 static const std::string token = getEnvOrDefault("ATTENDANCE_API_TOKEN", DEFAULT_API_TOKEN); return token; } // 函数作用:返回允许跨域访问的来源配置。 const std::string& allowedCorsOrigin() { // CORS 允许来源,供 Qt/Web 管理端跨域访问 API。 static const std::string origin = getEnvOrDefault("ATTENDANCE_CORS_ORIGIN", DEFAULT_CORS_ORIGIN); return origin; } // 函数作用:返回 HTTP 服务监听的绑定地址。 const std::string& bindAddress() { // 默认监听 0.0.0.0,方便局域网内 K210、STM32 和管理端访问。 // 如果只想本机调试,可设置 ATTENDANCE_BIND_HOST=127.0.0.1。 static const std::string address = getEnvOrDefault("ATTENDANCE_BIND_HOST", "0.0.0.0"); return address; } std::string readStudents(); std::string readClasses(); std::string readFaceProfiles(); std::string getPendingDeleteList(); std::string getFacePhotosList(); bool setSyncRequest(bool sync); bool removeFromPendingDelete(const std::string& face_id); bool cancelPendingDeleteForFace(sqlite3* db, const std::string& face_id, const std::string& now); bool updateDeviceSyncState(const std::string& client, unsigned long long seen_version, unsigned long long ack_version); std::string getDeviceSyncSummary(); bool initializeSqliteStorage(); bool saveAttendanceRecord(const std::string& timestamp, const std::string& student_id, const std::string& name, const std::string& confidence, const std::string& status, const std::string& class_id); bool saveStudents(const std::string& json_data); bool saveClasses(const std::string& json_data); bool saveFaceProfiles(const std::string& json_data); unsigned long long bumpSyncVersion(const std::string& reason); std::vector> parseJsonObjectArray(const std::string& json_data, const std::string& key); std::string extractJsonMemberValue(const std::string& json, const std::string& key, const std::string& fallback); // 函数作用:转义字符串,使其可以安全写入 JSON 字符串字段。 std::string jsonEscape(const std::string& value) { // 手工拼接 JSON 时必须转义引号、反斜杠和控制字符, // 否则学生姓名、班级名中包含特殊字符会导致返回 JSON 结构损坏。 std::ostringstream out; for (unsigned char c : value) { switch (c) { case '\"': out << "\\\""; break; case '\\': out << "\\\\"; break; case '\b': out << "\\b"; break; case '\f': out << "\\f"; break; case '\n': out << "\\n"; break; case '\r': out << "\\r"; break; case '\t': out << "\\t"; break; default: if (c < 0x20) { const char* hex = "0123456789abcdef"; out << "\\u00" << hex[(c >> 4) & 0x0f] << hex[c & 0x0f]; } else { out << static_cast(c); } break; } } return out.str(); } // 函数作用:转义 CSV 单元格内容,避免逗号、引号和换行破坏列结构。 std::string csvEscape(const std::string& value) { // CSV 字段中若包含逗号、引号或换行,需要用双引号包裹; // 字段内部的双引号按 CSV 规则写成两个双引号。 bool needs_quotes = false; for (char c : value) { if (c == ',' || c == '"' || c == '\r' || c == '\n') { needs_quotes = true; break; } } if (!needs_quotes) { return value; } std::string escaped = "\""; for (char c : value) { if (c == '"') { escaped += "\"\""; } else { escaped += c; } } escaped += "\""; return escaped; } // 函数作用:解析整数并校验其是否落在指定范围内。 bool parseIntInRange(const std::string& value, int min_value, int max_value, int& output) { // 从查询参数或请求头中解析整数,并限制取值范围。 // 例如 limit、offset、Content-Length 都不能无限大,否则会影响服务端稳定性。 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; } } // 函数作用:解析 HH:mm 格式时间并拆分为小时和分钟。 bool parseTimeParts(const std::string& value, int& hour, int& minute) { // 解析 HH:MM 形式的时间,用于时间段配置和重复签到判断。 // 只接受严格 5 字符格式,避免 8:0、24:00 等非标准输入进入后续计算。 if (value.size() != 5 || value[2] != ':' || !std::isdigit(static_cast(value[0])) || !std::isdigit(static_cast(value[1])) || !std::isdigit(static_cast(value[3])) || !std::isdigit(static_cast(value[4]))) { return false; } int parsed_hour = (value[0] - '0') * 10 + (value[1] - '0'); int parsed_minute = (value[3] - '0') * 10 + (value[4] - '0'); if (parsed_hour < 0 || parsed_hour > 23 || parsed_minute < 0 || parsed_minute > 59) { return false; } hour = parsed_hour; minute = parsed_minute; return true; } // 函数作用:校验编号字符串是否只包含允许用于路径和主键的安全字符。 bool isSafeId(const std::string& value) { // ID 安全校验:只允许字母、数字、下划线和短横线。 // 该函数用于 student_id、class_id、face_id 等字段,尤其防止照片路径穿越。 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) { // 由 face_id 生成照片路径前必须先做安全校验。 // 合法 face_id=12 时路径为 faces/12.jpg,非法值如 ../x 会被拒绝。 if (!isSafeId(face_id)) { return false; } filepath = FACES_DIR + "/" + face_id + ".jpg"; return true; } // 函数作用:判断文件是否存在且大小大于 0。 bool fileExistsAndNotEmpty(const std::string& filepath) { std::ifstream file(filepath.c_str(), std::ios::binary | std::ios::ate); return file.is_open() && file.tellg() > 0; } // 函数作用:从原始 HTTP 请求文本中提取指定请求头的值。 bool getHeaderValue(const std::string& request, const std::string& header_name, std::string& output) { // 从原始 HTTP 请求头中读取指定 header,大小写不敏感。 // 由于本服务没有使用第三方 HTTP 框架,这里需要自行完成基础解析。 size_t headers_end = request.find("\r\n\r\n"); std::string headers = headers_end == std::string::npos ? request : request.substr(0, headers_end); std::istringstream stream(headers); std::string line; std::string expected_key = header_name; std::transform(expected_key.begin(), expected_key.end(), expected_key.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); while (std::getline(stream, line)) { if (!line.empty() && line.back() == '\r') { line.pop_back(); } size_t colon = line.find(':'); if (colon == std::string::npos) { continue; } std::string key = line.substr(0, colon); std::string value = line.substr(colon + 1); key.erase(std::remove_if(key.begin(), key.end(), [](unsigned char c) { return std::isspace(c); }), key.end()); std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (key != expected_key) { continue; } size_t first = value.find_first_not_of(" \t"); if (first == std::string::npos) { output.clear(); } else { value.erase(0, first); size_t last = value.find_last_not_of(" \t"); if (last != std::string::npos) { value.erase(last + 1); } output = value; } return true; } return false; } // 函数作用:判断指定请求头中的 Token 是否与期望值一致。 bool hasHeaderToken(const std::string& request, const std::string& header_name, const std::string& expected) { // 校验自定义请求头中的令牌,例如 X-Api-Token。 // K210 的 HTTP 请求使用该方式鉴权,避免未授权设备直接写入考勤数据。 size_t headers_end = request.find("\r\n\r\n"); std::string headers = headers_end == std::string::npos ? request : request.substr(0, headers_end); std::istringstream stream(headers); std::string line; while (std::getline(stream, line)) { if (!line.empty() && line.back() == '\r') { line.pop_back(); } size_t colon = line.find(':'); if (colon == std::string::npos) { continue; } std::string key = line.substr(0, colon); std::string value = line.substr(colon + 1); key.erase(std::remove_if(key.begin(), key.end(), [](unsigned char c) { return std::isspace(c); }), key.end()); size_t first = value.find_first_not_of(" \t"); if (first == std::string::npos) { value.clear(); } else { value.erase(0, first); size_t last = value.find_last_not_of(" \t"); if (last != std::string::npos) { value.erase(last + 1); } } std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); std::string expected_key = header_name; std::transform(expected_key.begin(), expected_key.end(), expected_key.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (key == expected_key && value == expected) { return true; } } return false; } // 函数作用:判断 Authorization Bearer Token 是否有效。 bool hasBearerToken(const std::string& request, const std::string& expected) { // 兼容 Authorization: Bearer 形式。 // 管理端或调试工具使用标准 Bearer 头时,不必额外设置 X-Api-Token。 std::string authorization; if (!getHeaderValue(request, "Authorization", authorization)) { return false; } const std::string prefix = "Bearer "; if (authorization.size() <= prefix.size()) { return false; } std::string scheme = authorization.substr(0, prefix.size()); std::transform(scheme.begin(), scheme.end(), scheme.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (scheme != "bearer ") { return false; } return authorization.substr(prefix.size()) == expected; } // 函数作用:判断接口是否为无需鉴权的公开接口。 bool isPublicEndpoint(const std::string& method, const std::string& endpoint) { (void)endpoint; // 目前只有浏览器预检 OPTIONS 请求不要求令牌。 // 真正的业务接口全部位于 /api/v1/ 下,并要求通过 isAuthorizedRequest 校验。 return method == "OPTIONS"; } // 函数作用:综合请求路径和 Token 判断当前 HTTP 请求是否通过鉴权。 bool isAuthorizedRequest(const std::string& request, const std::string& method, const std::string& endpoint) { // 统一鉴权入口。 // 通过 X-Api-Token 或 Authorization Bearer 任意一种即可,便于嵌入式端和管理端复用。 if (isPublicEndpoint(method, endpoint)) { return true; } return hasHeaderToken(request, "X-Api-Token", apiToken()) || hasBearerToken(request, apiToken()); } // 函数作用:生成统一格式的 JSON 错误响应体。 std::string makeJsonError(const std::string& message) { return "{\"success\":false,\"message\":\"" + jsonEscape(message) + "\"}"; } // 函数作用:判断请求是否声明了 AES 加密请求体。 bool isEncryptedRequest(const std::string& request) { // 客户端在请求头中声明 X-Encrypted: AES128-CBC 时, // 服务端会把 body 先 Base64 解码再 AES 解密。 return hasHeaderToken(request, "X-Encrypted", "AES128-CBC"); } // 函数作用:判断 Content-Type 是否属于 JSON 或可按 JSON 处理的类型。 bool isJsonLikeContentType(const std::string& content_type) { // 判断响应体是否是 JSON。 // 只有 JSON 响应会被自动加密,图片等二进制内容保持原始格式返回。 std::string lower = content_type; std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return lower.find("application/json") != std::string::npos || lower.find("text/json") != std::string::npos || lower.find("+json") != std::string::npos; } // 重要:密钥和 IV 必须与 K210/STM32 端完全一致。 // 本系统使用 AES-128-CBC: // - KEY 长度 16 字节,对应 AES-128; // - IV 长度 16 字节,CBC 模式每次加解密都需要; // - 当前为固定 IV,适合本毕业设计演示和端侧兼容,生产环境应改成随机 IV 并随报文传输。 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解码 ============= // 函数作用:将 Base64 字符串解码为原始二进制字符串。 std::string base64_decode(const std::string& encoded_string) { // OpenSSL BIO 方式解码 Base64。 // 客户端加密后的二进制密文不能直接放入 HTTP 文本 body,因此先 Base64 编码。 BIO *bio, *b64; int decode_len = static_cast(encoded_string.length()); if (decode_len <= 0) { return ""; } char* buffer = new char[decode_len]; bio = BIO_new_mem_buf(encoded_string.c_str(), -1); 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); if (length <= 0) { delete[] buffer; return ""; } std::string result(buffer, length); delete[] buffer; return result; } // ============= 添加PKCS#7填充 ============= // 函数作用:为 AES-CBC 明文追加 PKCS#7 填充。 std::string add_pkcs7_padding(const std::string& data) { // AES-CBC 的输入长度必须是 16 字节整数倍。 // PKCS#7 填充会补 N 个值为 N 的字节,即使原文正好是 16 的倍数也会补一整块。 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填充 ============= // 函数作用:验证并移除 AES-CBC 解密结果中的 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; // 返回原数据 } } // 移除填充后得到原始 JSON 字符串。 return data.substr(0, data.length() - padding_len); } // ============= AES-CBC加密 (用于响应Qt客户端) ============= // 函数作用:使用 AES-128-CBC 对明文进行加密。 std::string aes_encrypt_cbc(const std::string& plaintext) { // 响应加密流程:JSON 明文 -> PKCS#7 填充 -> AES-CBC 加密 -> Base64。 // 本函数只负责 AES-CBC 二进制加密,Base64 在 encryptPayload 中处理。 // 添加 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 ""; } // 初始化 OpenSSL EVP 加密上下文,算法固定为 AES-128-CBC。 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 ""; } // 禁用 OpenSSL 自动填充,确保和 MaixPy/STM32 端的手动 PKCS#7 行为一致。 EVP_CIPHER_CTX_set_padding(ctx, 0); // 加密数据。输出缓冲区预留额外 16 字节,满足 EVP 接口要求。 std::vector ciphertext(padded.length() + 16); int len; int ciphertext_len = 0; if (EVP_EncryptUpdate(ctx, ciphertext.data(), &len, (unsigned char*)padded.c_str(), padded.length()) != 1) { std::cerr << "[CRYPTO] EncryptUpdate failed" << std::endl; EVP_CIPHER_CTX_free(ctx); return ""; } ciphertext_len = len; // 完成加密并释放上下文。 if (EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len) != 1) { std::cerr << "[CRYPTO] EncryptFinal failed" << std::endl; EVP_CIPHER_CTX_free(ctx); return ""; } ciphertext_len += len; EVP_CIPHER_CTX_free(ctx); return std::string((char*)ciphertext.data(), ciphertext_len); } // ============= AES-CBC解密 (使用OpenSSL标准库) ============= // 函数作用:使用 AES-128-CBC 对密文进行解密。 std::string aes_decrypt_cbc(const std::string& encrypted_data) { // 请求解密流程:Base64 body -> AES-CBC 解密 -> 去除 PKCS#7 填充 -> JSON 明文。 // 这里要求密文长度必须是 16 字节整数倍,否则说明 Base64 或传输内容不完整。 if (encrypted_data.empty() || encrypted_data.length() % 16 != 0) { std::cerr << "[CRYPTO] Invalid encrypted data length: " << encrypted_data.length() << std::endl; return ""; } EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); if (!ctx) { std::cerr << "[CRYPTO] Failed to create context" << std::endl; return ""; } // 初始化解密(使用标准 AES-128-CBC)。 if (EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, MY_AES_KEY, MY_AES_IV) != 1) { std::cerr << "[CRYPTO] DecryptInit failed" << std::endl; EVP_CIPHER_CTX_free(ctx); return ""; } // 禁用自动填充,我们手动处理 PKCS#7,保持两端行为一致。 EVP_CIPHER_CTX_set_padding(ctx, 0); // 解密数据。 std::vector plaintext(encrypted_data.length() + 16); int len; int plaintext_len = 0; if (EVP_DecryptUpdate(ctx, plaintext.data(), &len, (unsigned char*)encrypted_data.c_str(), encrypted_data.length()) != 1) { std::cerr << "[CRYPTO] DecryptUpdate failed" << std::endl; EVP_CIPHER_CTX_free(ctx); return ""; } plaintext_len = len; // 完成解密。 if (EVP_DecryptFinal_ex(ctx, plaintext.data() + len, &len) != 1) { std::cerr << "[CRYPTO] DecryptFinal failed" << std::endl; EVP_CIPHER_CTX_free(ctx); return ""; } plaintext_len += len; EVP_CIPHER_CTX_free(ctx); std::string result((char*)plaintext.data(), plaintext_len); // 手动移除 PKCS#7 填充。 result = remove_pkcs7_padding(result); return result; } // ============= 十六进制输出辅助函数 ============= // 函数作用:按十六进制格式打印调试用二进制数据。 void print_hex(const std::string& label, const unsigned char* data, size_t len) { // 调试用:只打印前 32 字节,既能确认数据形态,也避免长密文刷屏。 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编码 ============= // 函数作用:将原始二进制字符串编码为 Base64 字符串。 std::string base64_encode(const std::string& input) { // 将 AES 二进制密文编码成无换行 Base64 文本,便于作为 HTTP body 传输。 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; } // ============= 加密传输数据 ============= // 函数作用:加密通用响应或请求载荷并输出 Base64。 std::string encryptPayload(const std::string& plain_data) { // 统一的响应加密封装。 // sendResponse 会在返回 JSON 前调用该函数,并追加 X-Encrypted 响应头。 std::cout << "[CRYPTO] Encrypting payload (" << plain_data.size() << " bytes)..." << std::endl; // AES 加密。 std::string encrypted = aes_encrypt_cbc(plain_data); if (encrypted.empty()) { std::cerr << "[CRYPTO] Encryption failed!" << std::endl; return ""; } // Base64 编码。 std::string encrypted_base64 = base64_encode(encrypted); std::cout << "[CRYPTO] ✓ Payload encrypted" << std::endl; return encrypted_base64; } // 函数作用:加密 JSON 响应体并输出 Base64 字符串。 std::string encryptJSON(const std::string& json_data) { return encryptPayload(json_data); } // ============= 解密传输数据 ============= // 函数作用:解密 Base64 编码的通用请求载荷。 std::string decryptPayload(const std::string& encrypted_base64) { // 统一的请求解密封装。 // POST JSON 接口要求客户端加密 body;照片上传接口可以不加密,因为照片体积较大。 try { std::cout << "[CRYPTO] Decrypting payload..." << 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()); return decrypted; } catch (const std::exception& e) { std::cerr << "[CRYPTO] Exception: " << e.what() << std::endl; return ""; } } // 函数作用:解密 Base64 编码的加密 JSON 请求体。 std::string decryptJSON(const std::string& encrypted_base64) { return decryptPayload(encrypted_base64); } // ============= 工具函数 ============= // 函数作用:获取当前本地时间戳字符串。 std::string getCurrentTimestamp() { // 返回本地时间字符串,作为考勤记录、同步任务、照片上传等表的时间字段。 // Windows 和 Linux 的 localtime 接口不同,因此用条件编译做兼容。 time_t now = time(0); tm ltm; #ifdef _WIN32 localtime_s(<m, &now); #else localtime_r(&now, <m); #endif char buffer[80]; strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", <m); return std::string(buffer); } // 清理字符串中的控制字符(\r, \n等) // 函数作用:清理字符串首尾空白和控制字符。 std::string cleanString(const std::string& str) { // 过滤串口或 HTTP 输入中可能混入的控制字符,避免写入 JSON/CSV 时破坏格式。 std::string result; result.reserve(str.length()); for (char c : str) { // 只保留可打印字符和空格,过滤掉\r\n\t等控制字符 if (c >= 32 && c <= 126) { result += c; } } return result; } // 函数作用:对 URL 编码字符串进行解码。 std::string urlDecode(const std::string& str) { // 解析 URL 中的百分号编码和 + 空格。 // 删除接口和查询参数中可能包含编码后的学号、face_id、时间等字段。 std::string result; for (size_t i = 0; i < str.length(); ++i) { if (str[i] == '%' && i + 2 < str.length()) { int value; std::istringstream is(str.substr(i + 1, 2)); if (is >> std::hex >> value) { result += static_cast(value); i += 2; } else { result += str[i]; } } else if (str[i] == '+') { result += ' '; } else { result += str[i]; } } return result; } // 函数作用:解析 URL 查询字符串为键值参数表。 std::map parseQueryParams(const std::string& query) { // 将 a=1&b=2 形式的查询字符串解析为 map,供分页、时间段、删除选项等接口使用。 std::map params; std::istringstream stream(query); std::string pair; while (std::getline(stream, pair, '&')) { size_t pos = pair.find('='); if (pos != std::string::npos) { std::string key = urlDecode(pair.substr(0, pos)); std::string value = urlDecode(pair.substr(pos + 1)); params[key] = value; } } return params; } // 函数作用:从 JSON 文本当前位置解析一个字符串字面量。 bool parseJsonStringLiteral(const std::string& json, size_t& pos, std::string& output) { // 解析 JSON 字符串字面量,支持常见转义字符。 // 这是后续轻量 JSON 解析器的基础能力。 if (pos >= json.size() || json[pos] != '"') return false; pos++; output.clear(); while (pos < json.size()) { char c = json[pos++]; if (c == '"') { return true; } if (c != '\\') { output += c; continue; } if (pos >= json.size()) return false; char esc = json[pos++]; switch (esc) { case '"': output += '"'; break; case '\\': output += '\\'; break; case '/': output += '/'; break; case 'b': output += '\b'; break; case 'f': output += '\f'; break; case 'n': output += '\n'; break; case 'r': output += '\r'; break; case 't': output += '\t'; break; case 'u': // 当前接口字段只需要保留 UTF-8 原文;遇到 unicode 转义时不执行错误拆分。 output += "\\u"; for (int i = 0; i < 4 && pos < json.size(); ++i) { output += json[pos++]; } break; default: output += esc; break; } } return false; } // 解析签到接口使用的扁平 JSON 对象,支持字符串转义、逗号和冒号。 // 函数作用:解析简单 JSON 对象为字符串键值表。 std::map parseJSON(const std::string& json) { // 本项目接口的 JSON 结构比较固定,为降低依赖复杂度没有引入完整 JSON 库。 // 该函数用于解析 {"student_id":"...","confidence":0.95} 这类扁平对象。 std::map result; size_t pos = json.find('{'); if (pos == std::string::npos) return result; pos++; while (pos < json.size()) { while (pos < json.size() && std::isspace(static_cast(json[pos]))) pos++; if (pos >= json.size() || json[pos] == '}') break; std::string key; if (!parseJsonStringLiteral(json, pos, key)) { result.clear(); return result; } while (pos < json.size() && std::isspace(static_cast(json[pos]))) pos++; if (pos >= json.size() || json[pos] != ':') { result.clear(); return result; } pos++; while (pos < json.size() && std::isspace(static_cast(json[pos]))) pos++; std::string value; if (pos < json.size() && json[pos] == '"') { if (!parseJsonStringLiteral(json, pos, value)) { result.clear(); return result; } } else { size_t start = pos; while (pos < json.size() && json[pos] != ',' && json[pos] != '}') pos++; value = json.substr(start, pos - start); size_t first = value.find_first_not_of(" \t\r\n"); size_t last = value.find_last_not_of(" \t\r\n"); value = first == std::string::npos ? "" : value.substr(first, last - first + 1); } result[key] = value; while (pos < json.size() && std::isspace(static_cast(json[pos]))) pos++; if (pos < json.size() && json[pos] == ',') pos++; } return result; } // ============= SQLite存储层 ============= // 服务端所有运行时数据统一存放在 attendance.db 中。 // 使用 SQLite 的原因: // - 单文件部署简单,适合毕业设计演示和小规模课堂考勤场景; // - 支持事务、索引和 WAL,能够满足管理端查询与终端写入并发; // - 方便备份和随项目一起迁移。 // 函数作用:打开 SQLite 数据库连接。 sqlite3* openSqliteDb() { // 每个请求按需打开数据库连接,用完立即关闭。 // sqlite3_busy_timeout 用于在 WAL/写事务短暂占用时等待,而不是立即失败。 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; } // 函数作用:执行不返回结果集的 SQLite 语句。 bool sqliteExec(sqlite3* db, const std::string& sql) { // 执行不需要返回结果的 SQL,例如建表、事务 BEGIN/COMMIT、批量删除。 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; } // 函数作用:安全读取 SQLite 查询结果中的文本列。 std::string sqliteColumnText(sqlite3_stmt* stmt, int col) { // 安全读取 SQLite 文本列。NULL 值统一转为空字符串,便于 JSON 拼接。 const unsigned char* text = sqlite3_column_text(stmt, col); return text ? reinterpret_cast(text) : ""; } // 函数作用:把字符串参数绑定到 SQLite 预编译语句。 bool sqliteBindText(sqlite3_stmt* stmt, int index, const std::string& value) { // 绑定文本参数时使用 SQLITE_TRANSIENT,让 SQLite 自己复制字符串内容。 // 这样本地 std::string 生命周期结束后,SQL 执行仍然安全。 return sqlite3_bind_text(stmt, index, value.c_str(), -1, SQLITE_TRANSIENT) == SQLITE_OK; } // 函数作用:以二进制方式复制文件。 bool copyFileBinary(const std::string& src, const std::string& dst) { // 启动备份使用二进制复制,避免不同平台换行转换影响 SQLite 文件。 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(); } // 函数作用:服务启动时备份 SQLite 数据库文件。 bool backupSqliteDatabaseOnStartup() { // 服务启动前先备份现有数据库。 // 如果后续版本升级或误操作导致数据异常,可以从 backups 目录恢复。 std::ifstream check(SQLITE_DB_FILE.c_str(), std::ios::binary); if (!check.is_open()) { return true; } check.close(); _mkdir(DB_BACKUP_DIR.c_str()); std::string ts = getCurrentTimestamp(); std::replace(ts.begin(), ts.end(), ':', '-'); std::replace(ts.begin(), ts.end(), ' ', '_'); const std::string backup_path = DB_BACKUP_DIR + "/attendance_" + ts + ".db"; if (!copyFileBinary(SQLITE_DB_FILE, backup_path)) { std::cerr << "[BACKUP] Failed to create SQLite backup: " << backup_path << std::endl; return false; } std::cout << "[BACKUP] SQLite backup created: " << backup_path << std::endl; return true; } // 函数作用:在指定数据库连接中写入键值配置。 bool sqliteSetKv(sqlite3* db, const std::string& key, const std::string& value) { // kv_store 用来保存少量全局配置,例如 time_slots、sync_request、sync_version。 // REPLACE INTO 可同时覆盖旧值和插入新值。 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) { // 读取 kv_store,若不存在则返回调用方给定的 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; } // 函数作用:统计指定 SQLite 表中的记录行数。 int sqliteCountRows(sqlite3* db, const std::string& table) { // 统计表行数,用于 health/storage 接口展示服务端状态。 sqlite3_stmt* stmt = nullptr; std::string sql = "SELECT COUNT(*) FROM " + table; int count = 0; if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK && sqlite3_step(stmt) == SQLITE_ROW) { count = sqlite3_column_int(stmt, 0); } sqlite3_finalize(stmt); return count; } // 函数作用:归一化班级编号,空值时使用默认班级编号。 std::string normalizedClassId(const std::string& class_id) { // 空班级统一归入 "0",即默认班级。 return class_id.empty() ? "0" : class_id; } // 函数作用:根据班级编号生成默认班级名称。 std::string defaultClassName(const std::string& class_id) { // 默认班级给出中文显示名,其余班级若未提供名称则直接使用 class_id。 return class_id == "0" ? "默认班级" : class_id; } // 函数作用:新增或更新班级信息。 bool upsertClass(sqlite3* db, const std::string& class_id, const std::string& class_name, const std::string& now) { // 新增或更新班级。 // 若班级已存在,只更新非空名称和更新时间;避免空字段覆盖已有配置。 if (!db) return false; const std::string normalized_id = normalizedClassId(class_id); const std::string normalized_name = class_name.empty() ? defaultClassName(normalized_id) : class_name; sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO classes(class_id,class_name,updated_at) VALUES(?,?,?) " "ON CONFLICT(class_id) DO UPDATE SET " "class_name=CASE WHEN excluded.class_name<>'' THEN excluded.class_name ELSE classes.class_name END," "updated_at=excluded.updated_at"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false; sqliteBindText(stmt, 1, normalized_id); sqliteBindText(stmt, 2, normalized_name); sqliteBindText(stmt, 3, now); bool ok = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return ok; } // 函数作用:确保指定班级在数据库中存在。 bool ensureClassExists(sqlite3* db, const std::string& class_id, const std::string& now) { // 写入学生时如果指定了新班级,先确保班级记录存在。 // 这样学生表的 class_id 总能在班级表中找到对应项。 if (!db) return false; const std::string normalized_id = normalizedClassId(class_id); sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT OR IGNORE INTO classes(class_id,class_name,updated_at) VALUES(?,?,?)"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false; sqliteBindText(stmt, 1, normalized_id); sqliteBindText(stmt, 2, defaultClassName(normalized_id)); sqliteBindText(stmt, 3, now); bool ok = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return ok; } bool upsertStudent(sqlite3* db, const std::string& student_id, const std::string& student_name, const std::string& class_id, const std::string& now) { // 新增或更新学生档案。 // 考勤记录上传时也会调用该函数,保证即使管理端未提前录入学生,也能自动生成基础档案。 if (!db || student_id.empty()) return false; const std::string insert_class_id = normalizedClassId(class_id); if (!class_id.empty() && !ensureClassExists(db, insert_class_id, now)) { return false; } sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO students(student_id,student_name,class_id,status,created_at,updated_at) " "VALUES(?,?,?,?,?,?) " "ON CONFLICT(student_id) DO UPDATE SET " "student_name=CASE WHEN excluded.student_name<>'' THEN excluded.student_name ELSE students.student_name END," "class_id=CASE WHEN ?<>'' THEN excluded.class_id ELSE students.class_id END," "status='active'," "updated_at=excluded.updated_at"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false; sqliteBindText(stmt, 1, student_id); sqliteBindText(stmt, 2, student_name); sqliteBindText(stmt, 3, insert_class_id); sqliteBindText(stmt, 4, "active"); sqliteBindText(stmt, 5, now); sqliteBindText(stmt, 6, now); sqliteBindText(stmt, 7, class_id); bool ok = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return ok; } bool upsertFaceProfile(sqlite3* db, const std::string& face_id, const std::string& student_id, const std::string& status, const std::string& now) { // 新增或更新人脸档案。 // face_id 是 K210 本地生成的原始人脸编号,student_id 是管理端绑定的真实学号。 // 二者分离后,K210 特征文件无需因为学号变更而重写。 if (!db || face_id.empty()) return false; sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO face_profiles(face_id,student_id,device_id,status,created_at,updated_at) " "VALUES(?,?,?,?,?,?) " "ON CONFLICT(face_id) DO UPDATE SET " "student_id=CASE WHEN excluded.student_id<>'' THEN excluded.student_id ELSE face_profiles.student_id END," "device_id=excluded.device_id," "status=excluded.status," "updated_at=excluded.updated_at"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false; sqliteBindText(stmt, 1, face_id); sqliteBindText(stmt, 2, student_id); sqliteBindText(stmt, 3, "k210-01"); sqliteBindText(stmt, 4, status.empty() ? "active" : status); sqliteBindText(stmt, 5, now); sqliteBindText(stmt, 6, now); bool ok = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return ok; } // 函数作用:清除指定人脸档案的学生绑定关系。 bool clearFaceProfileBinding(sqlite3* db, const std::string& face_id, const std::string& now) { // 解除 face_id 与 student_id 的绑定,但不一定删除人脸档案。 // 如果照片仍存在,状态保持 active;如果照片也不存在,状态标记 inactive。 if (!db || face_id.empty()) return false; sqlite3_stmt* stmt = nullptr; bool ok = false; const char* sql = "UPDATE face_profiles SET " "student_id=''," "status=CASE WHEN EXISTS(SELECT 1 FROM face_photos WHERE face_photos.face_id=face_profiles.face_id) " "THEN 'active' ELSE 'inactive' END," "updated_at=? " "WHERE face_id=?"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, now); sqliteBindText(stmt, 2, face_id); ok = sqlite3_step(stmt) == SQLITE_DONE; } sqlite3_finalize(stmt); return ok; } // 函数作用:获取指定文件大小。 sqlite3_int64 getFileSize(const std::string& filepath) { std::ifstream file(filepath.c_str(), std::ios::binary | std::ios::ate); if (!file.is_open()) return 0; return static_cast(file.tellg()); } // 函数作用:判断指定人脸照片记录和文件是否存在。 bool facePhotoExists(sqlite3* db, const std::string& face_id) { // 判断人脸照片是否真实存在。 // 先查数据库索引,再回退检查 faces/.jpg 文件,兼容数据库记录缺失但文件存在的情况。 if (!db || !isSafeId(face_id)) return false; sqlite3_stmt* stmt = nullptr; bool exists = false; if (sqlite3_prepare_v2(db, "SELECT file_path FROM face_photos WHERE face_id=?", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, face_id); if (sqlite3_step(stmt) == SQLITE_ROW) { exists = getFileSize(sqliteColumnText(stmt, 0)) > 0; } } sqlite3_finalize(stmt); if (!exists) { std::string filepath; exists = buildFacePhotoPath(face_id, filepath) && fileExistsAndNotEmpty(filepath); } return exists; } // 函数作用:判断指定人脸档案是否存在。 bool faceProfileExists(sqlite3* db, const std::string& face_id) { // 判断 face_profiles 表中是否有该 face_id。 if (!db || !isSafeId(face_id)) return false; sqlite3_stmt* stmt = nullptr; bool exists = false; if (sqlite3_prepare_v2(db, "SELECT 1 FROM face_profiles WHERE face_id=? LIMIT 1", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, face_id); exists = sqlite3_step(stmt) == SQLITE_ROW; } sqlite3_finalize(stmt); return exists; } std::vector unboundPhotoCandidates(sqlite3* db, const std::string& wrong_face_id, const std::string& student_id) { // 修复管理端误把 student_id 当 face_id 提交的情况。 // 如果存在唯一一张“未绑定或已绑定同一学生”的照片,就把映射转移到真实 face_id。 std::vector candidates; if (!db || student_id.empty()) return candidates; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "SELECT ph.face_id FROM face_photos ph " "LEFT JOIN face_profiles fp ON fp.face_id=ph.face_id " "WHERE ph.face_id<>? AND (COALESCE(fp.student_id,'')='' OR fp.student_id=?) " "ORDER BY ph.face_id", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, wrong_face_id); sqliteBindText(stmt, 2, student_id); while (sqlite3_step(stmt) == SQLITE_ROW) { const std::string candidate = sqliteColumnText(stmt, 0); if (isSafeId(candidate)) { candidates.push_back(candidate); } } } sqlite3_finalize(stmt); return candidates; } // 函数作用:只删除指定人脸档案记录,不删除照片文件。 bool deleteFaceProfileOnly(sqlite3* db, const std::string& face_id) { // 只删除 face_profiles 中的档案,不删除照片文件和 K210 端特征。 // 主要用于修复错误映射时移除旧的错误 face_id。 if (!db || !isSafeId(face_id)) return false; sqlite3_stmt* stmt = nullptr; bool ok = false; if (sqlite3_prepare_v2(db, "DELETE FROM face_profiles WHERE face_id=?", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, face_id); ok = sqlite3_step(stmt) == SQLITE_DONE; } sqlite3_finalize(stmt); return ok; } // 函数作用:根据本地照片记录修复或补齐人脸档案。 int reconcileFaceProfilesWithPhotos(sqlite3* db, const std::string& now) { // 对齐人脸档案与照片索引。 // 典型问题:管理端提交了以 student_id 作为 face_id 的档案,但 K210 上传照片用的是原始 face_id。 // 若能找到唯一候选照片,则自动修正映射,减少人工清理数据的成本。 if (!db) return 0; struct Row { std::string face_id; std::string student_id; }; std::vector rows; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "SELECT fp.face_id,COALESCE(fp.student_id,'') " "FROM face_profiles fp " "LEFT JOIN face_photos ph ON ph.face_id=fp.face_id " "WHERE COALESCE(fp.student_id,'')<>'' " "AND (ph.face_id IS NULL OR fp.face_id=fp.student_id)", -1, &stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { rows.push_back({sqliteColumnText(stmt, 0), sqliteColumnText(stmt, 1)}); } } sqlite3_finalize(stmt); int fixed = 0; for (const Row& row : rows) { const std::vector candidates = unboundPhotoCandidates(db, row.face_id, row.student_id); if (candidates.size() != 1) { continue; } const std::string& real_face_id = candidates.front(); if (upsertFaceProfile(db, real_face_id, row.student_id, "active", now) && deleteFaceProfileOnly(db, row.face_id)) { fixed++; std::cout << "[FACE_PROFILE] Repaired mapping: wrong face_id " << row.face_id << " -> real face_id " << real_face_id << " for student " << row.student_id << std::endl; } } return fixed; } // 函数作用:执行 SQLite quick_check 并返回检查结果。 bool sqliteQuickCheck(std::string& message) { // SQLite 自带快速一致性检查,供 health/storage 接口判断数据库是否健康。 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() { // 返回 face_photos 表中的照片数量,用于系统状态展示。 sqlite3* db = openSqliteDb(); if (!db) return 0; int count = sqliteCountRows(db, "face_photos"); sqlite3_close(db); return count; } bool sqliteInsertRecord(sqlite3* db, const std::string& timestamp, const std::string& student_id, const std::string& name, const std::string& confidence, const std::string& status, const std::string& class_id) { // 写入一条考勤记录。 // 写入记录后同步 upsertStudent,保证记录中出现的学号也能在学生档案中查到。 sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO records(timestamp,student_id,name,confidence,status,class_id) " "VALUES(?,?,?,?,?,?)"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false; sqliteBindText(stmt, 1, timestamp); sqliteBindText(stmt, 2, student_id); sqliteBindText(stmt, 3, name); sqliteBindText(stmt, 4, confidence); sqliteBindText(stmt, 5, status); sqliteBindText(stmt, 6, class_id.empty() ? "0" : class_id); bool ok = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); if (!ok) return false; upsertStudent(db, student_id, name, class_id, timestamp); return true; } bool saveAttendanceRecord(const std::string& timestamp, const std::string& student_id, const std::string& name, const std::string& confidence, const std::string& status, const std::string& class_id) { // 对外封装的考勤记录保存函数,负责打开和关闭数据库。 sqlite3* db = openSqliteDb(); if (!db) return false; bool ok = sqliteInsertRecord(db, timestamp, student_id, name, confidence, status, class_id); sqlite3_close(db); return ok; } // 函数作用:初始化 SQLite 表结构、索引和默认配置。 bool initializeSqliteStorage() { // 初始化数据库结构。 // 所有 CREATE TABLE 都使用 IF NOT EXISTS,重复启动不会破坏已有数据。 // PRAGMA journal_mode=WAL 提高读写并发能力,管理端查询和终端写入可以更平滑共存。 sqlite3* db = openSqliteDb(); if (!db) return false; bool ok = sqliteExec(db, "PRAGMA journal_mode=WAL;" // records:考勤记录表,保存每一次签到上传结果。 "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);" // kv_store:轻量配置表,用于保存时间段、同步请求、同步版本号等全局状态。 "CREATE TABLE IF NOT EXISTS kv_store(" "key TEXT PRIMARY KEY," "value TEXT NOT NULL" ");" // classes:班级信息表。 "CREATE TABLE IF NOT EXISTS classes(" "class_id TEXT PRIMARY KEY," "class_name TEXT NOT NULL," "updated_at TEXT NOT NULL" ");" // students:学生档案表,status 支持 active/deleted,便于软删除。 "CREATE TABLE IF NOT EXISTS students(" "student_id TEXT PRIMARY KEY," "student_name TEXT," "class_id TEXT DEFAULT '0'," "status TEXT NOT NULL DEFAULT 'active'," "created_at TEXT NOT NULL," "updated_at TEXT NOT NULL" ");" "CREATE INDEX IF NOT EXISTS idx_students_class ON students(class_id);" // face_profiles:人脸档案表,维护 K210 face_id 与真实 student_id 的绑定。 "CREATE TABLE IF NOT EXISTS face_profiles(" "face_id TEXT PRIMARY KEY," "student_id TEXT," "device_id TEXT DEFAULT 'k210-01'," "status TEXT NOT NULL DEFAULT 'active'," "created_at TEXT NOT NULL," "updated_at TEXT NOT NULL" ");" "CREATE INDEX IF NOT EXISTS idx_face_profiles_student ON face_profiles(student_id);" // face_photos:人脸照片索引表,真实 JPEG 文件保存在 faces 目录。 "CREATE TABLE IF NOT EXISTS face_photos(" "face_id TEXT PRIMARY KEY," "file_path TEXT NOT NULL," "content_type TEXT DEFAULT 'image/jpeg'," "file_size INTEGER DEFAULT 0," "uploaded_at TEXT NOT NULL" ");" // pending_delete:等待 K210 同步执行的远程删除任务。 "CREATE TABLE IF NOT EXISTS pending_delete(" "task_id TEXT PRIMARY KEY," "face_id TEXT NOT NULL," "student_id TEXT," "created_at TEXT NOT NULL," "status TEXT NOT NULL DEFAULT 'pending'," "ack_at TEXT" ");" "CREATE INDEX IF NOT EXISTS idx_pending_delete_face ON pending_delete(face_id);" // device_sync_state:记录各客户端看到和确认的同步版本,便于管理端查看同步状态。 "CREATE TABLE IF NOT EXISTS device_sync_state(" "client TEXT PRIMARY KEY," "last_seen_version INTEGER NOT NULL DEFAULT 0," "last_ack_version INTEGER NOT NULL DEFAULT 0," "last_seen_at TEXT NOT NULL" ");"); if (!ok) { sqlite3_close(db); return false; } if (sqliteGetKv(db, "time_slots", "").empty()) { // 首次启动写入默认配置,避免管理端读取空值时报错。 sqliteSetKv(db, "time_slots", DEFAULT_TIMESLOTS_JSON); } if (sqliteGetKv(db, "sync_request", "").empty()) { sqliteSetKv(db, "sync_request", "{\"sync\":false}"); } if (sqliteGetKv(db, "sync_version", "").empty()) { sqliteSetKv(db, "sync_version", "1"); } const std::string now = getCurrentTimestamp(); upsertClass(db, "0", "默认班级", now); _mkdir(FACES_DIR.c_str()); sqlite3_close(db); std::cout << "[SQLITE] Storage ready: " << SQLITE_DB_FILE << std::endl; return true; } // ============= 数据库操作 ============= // 函数作用:分页读取考勤记录并生成 JSON 数组。 std::string readDatabase(int limit = -1, int offset = 0) { // 读取考勤记录列表,默认按最新记录在前返回。 // limit/offset 由管理端分页使用,避免一次性返回过多历史记录。 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) { // 保存签到时间段配置。 // STM32 会通过同步接口获取该配置,用于判断当前是否处于允许签到的时间段。 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; } // 函数作用:读取签到时间段配置 JSON。 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; } // 函数作用:读取学生档案列表并返回 JSON。 std::string readStudents() { // 返回未被软删除的学生列表。 // 管理端人脸绑定、班级筛选和记录展示都会使用这份学生档案。 std::ostringstream json; json << "{\"students\":["; sqlite3* db = openSqliteDb(); bool first = true; if (db) { sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "SELECT student_id,COALESCE(student_name,''),COALESCE(class_id,'0'),COALESCE(status,'active') " "FROM students WHERE student_id<>'' AND COALESCE(status,'active')<>'deleted' ORDER BY student_id", -1, &stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { if (!first) json << ","; json << "{\"student_id\":\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\"," << "\"student_name\":\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\"," << "\"class_id\":\"" << jsonEscape(sqliteColumnText(stmt, 2)) << "\"," << "\"status\":\"" << jsonEscape(sqliteColumnText(stmt, 3)) << "\"}"; first = false; } } sqlite3_finalize(stmt); sqlite3_close(db); } json << "]}"; return json.str(); } // 函数作用:读取班级配置列表并返回 JSON。 std::string readClasses() { // 返回班级列表,包括默认班级 "0"。 std::ostringstream json; json << "{\"classes\":["; sqlite3* db = openSqliteDb(); bool first = true; if (db) { sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "SELECT class_id,COALESCE(class_name,'') FROM classes WHERE class_id<>'' ORDER BY class_id", -1, &stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { if (!first) json << ","; json << "{\"class_id\":\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\"," << "\"class_name\":\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\"}"; first = false; } } sqlite3_finalize(stmt); sqlite3_close(db); } json << "]}"; return json.str(); } // 函数作用:读取人脸档案及照片状态并返回 JSON。 std::string readFaceProfiles() { // 返回人脸档案列表。 // 该接口会合并 face_profiles 和 face_photos: // - 已有档案和照片的 face_id 正常返回; // - 只有照片但尚未绑定学生的 face_id 以 status=photo_only 返回; // - 返回前会尝试修复“档案 face_id 与照片 face_id 不一致”的历史数据。 std::ostringstream json; json << "{\"face_profiles\":["; sqlite3* db = openSqliteDb(); bool first = true; int repaired = 0; if (db) { repaired = reconcileFaceProfilesWithPhotos(db, getCurrentTimestamp()); if (repaired > 0) { std::cout << "[FACE_PROFILE] Repaired " << repaired << " profile/photo mismatches before listing" << std::endl; } sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "SELECT fp.face_id,COALESCE(fp.student_id,''),COALESCE(fp.device_id,'k210-01'),COALESCE(fp.status,'active')," "CASE WHEN ph.face_id IS NULL THEN 0 ELSE 1 END " "FROM face_profiles fp " "LEFT JOIN face_photos ph ON ph.face_id=fp.face_id " "WHERE fp.face_id<>'' " "UNION " "SELECT ph.face_id,'','k210-01','photo_only',1 FROM face_photos ph " "LEFT JOIN face_profiles fp ON fp.face_id=ph.face_id " "WHERE ph.face_id<>'' AND fp.face_id IS NULL " "ORDER BY face_id", -1, &stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { if (!first) json << ","; json << "{\"face_id\":\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\"," << "\"student_id\":\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\"," << "\"device_id\":\"" << jsonEscape(sqliteColumnText(stmt, 2)) << "\"," << "\"status\":\"" << jsonEscape(sqliteColumnText(stmt, 3)) << "\"," << "\"has_photo\":" << (sqlite3_column_int(stmt, 4) ? "true" : "false") << "}"; first = false; } } sqlite3_finalize(stmt); sqlite3_close(db); } if (repaired > 0) { bumpSyncVersion("face_profile_repair"); } json << "]}"; return json.str(); } // 函数作用:保存管理端上传的学生档案列表。 bool saveStudents(const std::string& json_data) { // 保存管理端上传的学生档案数组。 // 使用事务保证整批学生要么全部成功,要么全部回滚,避免半更新。 const std::vector> students = parseJsonObjectArray(json_data, "students"); sqlite3* db = openSqliteDb(); if (!db) return false; const std::string now = getCurrentTimestamp(); bool ok = sqliteExec(db, "BEGIN"); if (ok) { for (const auto& student : students) { const auto id_it = student.find("student_id"); const std::string student_id = id_it == student.end() ? "" : id_it->second; if (!isSafeId(student_id)) { ok = false; break; } const std::string name = student.count("student_name") ? student.at("student_name") : ""; const std::string class_id = student.count("class_id") ? normalizedClassId(student.at("class_id")) : "0"; if (!isSafeId(class_id) || name.size() > 64 || !upsertStudent(db, student_id, name, class_id, now)) { ok = false; break; } } } if (ok) { // 将本次上传列表中不存在的旧学生标记为 deleted。 // 同时解除这些学生的人脸绑定,防止 K210 继续把已删除学生作为有效身份上报。 sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "UPDATE students SET status='deleted',updated_at=? WHERE student_id=?", -1, &stmt, nullptr) == SQLITE_OK) { sqlite3_stmt* clear_face_stmt = nullptr; const bool clear_face_ready = sqlite3_prepare_v2(db, "UPDATE face_profiles SET student_id='',status=CASE " "WHEN EXISTS(SELECT 1 FROM face_photos WHERE face_photos.face_id=face_profiles.face_id) " "THEN 'active' ELSE 'unbound' END,updated_at=? WHERE student_id=?", -1, &clear_face_stmt, nullptr) == SQLITE_OK; std::vector existing_ids; sqlite3_stmt* select_stmt = nullptr; if (sqlite3_prepare_v2(db, "SELECT student_id FROM students WHERE COALESCE(status,'active')<>'deleted'", -1, &select_stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(select_stmt) == SQLITE_ROW) { existing_ids.push_back(sqliteColumnText(select_stmt, 0)); } } sqlite3_finalize(select_stmt); std::set incoming_ids; for (const auto& student : students) { auto it = student.find("student_id"); if (it != student.end()) incoming_ids.insert(it->second); } for (const std::string& student_id : existing_ids) { if (student_id == "UNKNOWN" || incoming_ids.count(student_id)) continue; sqlite3_reset(stmt); sqlite3_clear_bindings(stmt); sqliteBindText(stmt, 1, now); sqliteBindText(stmt, 2, student_id); if (sqlite3_step(stmt) != SQLITE_DONE) { ok = false; break; } if (clear_face_ready) { sqlite3_reset(clear_face_stmt); sqlite3_clear_bindings(clear_face_stmt); sqliteBindText(clear_face_stmt, 1, now); sqliteBindText(clear_face_stmt, 2, student_id); if (sqlite3_step(clear_face_stmt) != SQLITE_DONE) { ok = false; break; } } } sqlite3_finalize(clear_face_stmt); } else { ok = false; } sqlite3_finalize(stmt); } sqliteExec(db, ok ? "COMMIT" : "ROLLBACK"); sqlite3_close(db); return ok; } // 函数作用:保存管理端上传的班级配置列表。 bool saveClasses(const std::string& json_data) { // 保存班级配置。 // 删除班级前会检查该班级下是否仍有 active 学生,避免出现学生指向不存在班级的情况。 const std::vector> classes = parseJsonObjectArray(json_data, "classes"); sqlite3* db = openSqliteDb(); if (!db) return false; const std::string now = getCurrentTimestamp(); bool ok = sqliteExec(db, "BEGIN"); std::set incoming_ids; if (ok) { for (const auto& row : classes) { const std::string class_id = row.count("class_id") ? normalizedClassId(row.at("class_id")) : ""; const std::string class_name = row.count("class_name") ? row.at("class_name") : ""; if (!isSafeId(class_id) || class_name.empty() || class_name.size() > 64) { ok = false; break; } incoming_ids.insert(class_id); if (!upsertClass(db, class_id, class_name, now)) { ok = false; break; } } } if (ok) { sqlite3_stmt* select_stmt = nullptr; sqlite3_stmt* count_stmt = nullptr; sqlite3_stmt* delete_stmt = nullptr; std::vector existing_ids; if (sqlite3_prepare_v2(db, "SELECT class_id FROM classes WHERE class_id<>'0'", -1, &select_stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(select_stmt) == SQLITE_ROW) { existing_ids.push_back(sqliteColumnText(select_stmt, 0)); } } sqlite3_finalize(select_stmt); if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM students WHERE class_id=? AND COALESCE(status,'active')<>'deleted'", -1, &count_stmt, nullptr) != SQLITE_OK || sqlite3_prepare_v2(db, "DELETE FROM classes WHERE class_id=?", -1, &delete_stmt, nullptr) != SQLITE_OK) { ok = false; } for (const std::string& class_id : existing_ids) { if (!ok || incoming_ids.count(class_id)) continue; sqlite3_reset(count_stmt); sqlite3_clear_bindings(count_stmt); sqliteBindText(count_stmt, 1, class_id); int student_count = 0; if (sqlite3_step(count_stmt) == SQLITE_ROW) { student_count = sqlite3_column_int(count_stmt, 0); } if (student_count > 0) { ok = false; break; } sqlite3_reset(delete_stmt); sqlite3_clear_bindings(delete_stmt); sqliteBindText(delete_stmt, 1, class_id); if (sqlite3_step(delete_stmt) != SQLITE_DONE) { ok = false; break; } } sqlite3_finalize(count_stmt); sqlite3_finalize(delete_stmt); } sqliteExec(db, ok ? "COMMIT" : "ROLLBACK"); sqlite3_close(db); return ok; } // 函数作用:保存管理端上传的人脸档案绑定关系。 bool saveFaceProfiles(const std::string& json_data) { // 保存管理端提交的人脸绑定关系。 // 核心规则: // - face_id 必须来自 K210 上传的人脸照片或已有档案; // - student_id 为空表示解除绑定; // - student_id 非空表示绑定到真实学生学号,并自动确保学生档案存在; // - 成功后由路由层推进 sync_version,并设置 K210 同步请求标志。 const std::vector> profiles = parseJsonObjectArray(json_data, "face_profiles"); sqlite3* db = openSqliteDb(); if (!db) return false; const std::string now = getCurrentTimestamp(); bool ok = sqliteExec(db, "BEGIN"); std::set incoming_face_ids; std::set bound_face_ids; if (ok) { for (const auto& profile : profiles) { const std::string face_id = profile.count("face_id") ? profile.at("face_id") : ""; const std::string student_id = profile.count("student_id") ? profile.at("student_id") : ""; const std::string status = profile.count("status") ? profile.at("status") : "active"; if (!isSafeId(face_id) || (!student_id.empty() && !isSafeId(student_id))) { ok = false; break; } std::string effective_face_id = face_id; if (!student_id.empty() && !facePhotoExists(db, effective_face_id)) { // 如果提交的 face_id 没有照片,尝试按 student_id 找唯一未绑定照片做纠正。 // 这是为了兼容管理端或历史数据误把学号当 face_id 的情况。 const std::vector candidates = unboundPhotoCandidates(db, effective_face_id, student_id); if (candidates.size() == 1) { effective_face_id = candidates.front(); deleteFaceProfileOnly(db, face_id); std::cout << "[FACE_PROFILE] Corrected submitted face_id " << face_id << " -> " << effective_face_id << " for student " << student_id << std::endl; } else { std::cerr << "[FACE_PROFILE] Reject mapping without K210 photo: face_id=" << face_id << ", student_id=" << student_id << std::endl; ok = false; break; } } if (student_id.empty() && bound_face_ids.count(effective_face_id)) { incoming_face_ids.insert(effective_face_id); continue; } if (!student_id.empty()) { // 重新绑定人脸时,取消该 face_id 之前可能存在的待删除任务。 cancelPendingDeleteForFace(db, effective_face_id, now); } incoming_face_ids.insert(effective_face_id); if (!upsertFaceProfile(db, effective_face_id, student_id, status.empty() ? "active" : status, now)) { ok = false; break; } if (!student_id.empty()) { bound_face_ids.insert(effective_face_id); } if (student_id.empty()) { if (!clearFaceProfileBinding(db, effective_face_id, now)) { ok = false; break; } } if (!student_id.empty()) { if (!upsertStudent(db, student_id, "", "", now)) { ok = false; break; } } } } if (ok) { // 本次上传列表中未出现的旧 face_id 不直接删除,只解除绑定。 // 这样不会误删 K210 本地特征和照片,后续仍可重新绑定。 sqlite3_stmt* clear_stmt = nullptr; if (sqlite3_prepare_v2(db, "UPDATE face_profiles SET student_id='',status=CASE WHEN EXISTS(SELECT 1 FROM face_photos WHERE face_photos.face_id=face_profiles.face_id) THEN 'active' ELSE 'unbound' END,updated_at=? WHERE face_id=?", -1, &clear_stmt, nullptr) == SQLITE_OK) { sqlite3_stmt* select_stmt = nullptr; std::vector existing_ids; if (sqlite3_prepare_v2(db, "SELECT face_id FROM face_profiles", -1, &select_stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(select_stmt) == SQLITE_ROW) { existing_ids.push_back(sqliteColumnText(select_stmt, 0)); } } sqlite3_finalize(select_stmt); for (const std::string& face_id : existing_ids) { if (incoming_face_ids.count(face_id)) continue; sqlite3_reset(clear_stmt); sqlite3_clear_bindings(clear_stmt); sqliteBindText(clear_stmt, 1, now); sqliteBindText(clear_stmt, 2, face_id); if (sqlite3_step(clear_stmt) != SQLITE_DONE) { ok = false; break; } } } else { ok = false; } sqlite3_finalize(clear_stmt); } sqliteExec(db, ok ? "COMMIT" : "ROLLBACK"); sqlite3_close(db); return ok; } // 函数作用:读取当前统一同步版本号。 unsigned long long readSyncVersion() { // 读取全局同步版本号。 // 版本号从 1 开始,终端通过 last_version 判断自己是否落后。 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) { // 所有会影响终端状态的操作都调用该函数推进版本号。 // reason 只用于日志,方便排查是哪类操作触发了同步。 unsigned long long version = readSyncVersion() + 1; sqliteSetKvValue("sync_version", std::to_string(version)); std::cout << "[SYNC] Version bumped to " << version << " (" << reason << ")" << std::endl; return version; } // 函数作用:从查询参数中解析客户端传入的版本号。 unsigned long long parseVersionParam(const std::map& params) { // 解析 /api/v1/sync/state?last_version=... 参数。 // 非数字输入按 0 处理,代表客户端需要完整同步。 auto it = params.find("last_version"); if (it == params.end()) return 0; unsigned long long value = 0; for (char c : it->second) { if (!std::isdigit(static_cast(c))) return 0; value = value * 10 + static_cast(c - '0'); } return value; } // 函数作用:查找 JSON 对象或数组从指定位置开始的匹配结束位置。 size_t findMatchingJsonEnd(const std::string& text, size_t start) { // 在 JSON 字符串中查找与 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; } // 函数作用:从 JSON 文本中提取指定字段的原始值片段。 std::string extractJsonMemberValue(const std::string& json, const std::string& key, const std::string& fallback) { // 从固定结构 JSON 中提取指定成员的原始值。 // 例如从 {"face_profiles":[...]} 中提取 [...],用于组合统一同步响应。 std::string marker = "\"" + key + "\":"; size_t pos = json.find(marker); if (pos == std::string::npos) return fallback; pos += marker.size(); while (pos < json.size() && std::isspace(static_cast(json[pos]))) pos++; if (pos >= json.size()) return fallback; if (json[pos] == '{' || json[pos] == '[') { size_t end = findMatchingJsonEnd(json, pos); if (end != std::string::npos) { return json.substr(pos, end - pos + 1); } } size_t end = json.find_first_of(",}", pos); if (end != std::string::npos) { return json.substr(pos, end - pos); } return fallback; } // 函数作用:从 JSON 文本中提取指定字符串字段。 std::string extractJsonStringValue(const std::string& json, const std::string& key, const std::string& fallback = "") { // 提取简单字符串字段,适用于 sync ack 中的 client 等字段。 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); } // 函数作用:从 JSON 文本中提取字符串数组字段。 std::vector extractJsonStringArray(const std::string& json, const std::string& key) { // 提取简单字符串数组,例如 sync ack 中的 deleted 和 task_ids。 std::vector values; std::string arr = extractJsonMemberValue(json, key, "[]"); if (arr.size() < 2) return values; size_t pos = 0; while (pos < arr.size()) { size_t start = arr.find("\"", pos); if (start == std::string::npos) break; size_t end = arr.find("\"", start + 1); if (end == std::string::npos) break; values.push_back(arr.substr(start + 1, end - start - 1)); pos = end + 1; } return values; } // 函数作用:从 JSON 文本中解析对象数组字段。 std::vector> parseJsonObjectArray(const std::string& json_data, const std::string& key) { // 解析 {"students":[{...},{...}]} 这类对象数组。 // 学生、班级、人脸档案保存接口都复用该轻量解析器。 std::vector> rows; const std::string array = extractJsonMemberValue(json_data, key, "[]"); size_t pos = 0; while (pos < array.size()) { size_t start = array.find('{', pos); if (start == std::string::npos) break; size_t end = findMatchingJsonEnd(array, start); if (end == std::string::npos) break; const std::string object = array.substr(start, end - start + 1); std::map row; size_t item_pos = 1; while (item_pos < object.size()) { while (item_pos < object.size() && std::isspace(static_cast(object[item_pos]))) item_pos++; if (item_pos >= object.size() || object[item_pos] == '}') break; std::string item_key; if (!parseJsonStringLiteral(object, item_pos, item_key)) break; while (item_pos < object.size() && std::isspace(static_cast(object[item_pos]))) item_pos++; if (item_pos >= object.size() || object[item_pos] != ':') break; item_pos++; while (item_pos < object.size() && std::isspace(static_cast(object[item_pos]))) item_pos++; std::string item_value; if (item_pos < object.size() && object[item_pos] == '"') { if (!parseJsonStringLiteral(object, item_pos, item_value)) break; } else { size_t value_start = item_pos; while (item_pos < object.size() && object[item_pos] != ',' && object[item_pos] != '}') item_pos++; item_value = object.substr(value_start, item_pos - value_start); size_t first = item_value.find_first_not_of(" \t\r\n"); size_t last = item_value.find_last_not_of(" \t\r\n"); item_value = first == std::string::npos ? "" : item_value.substr(first, last - first + 1); } row[item_key] = item_value; while (item_pos < object.size() && std::isspace(static_cast(object[item_pos]))) item_pos++; if (item_pos < object.size() && object[item_pos] == ',') item_pos++; } rows.push_back(row); pos = end + 1; } return rows; } // 函数作用:从 JSON 文本中读取无符号整数值。 unsigned long long jsonNumberValue(const std::string& json, const std::string& key, unsigned long long fallback) { // 提取 JSON 中的非负整数字段,例如 ACK 请求体里的 version。 std::string marker = "\"" + key + "\":"; size_t pos = json.find(marker); if (pos == std::string::npos) return fallback; pos += marker.size(); while (pos < json.size() && std::isspace(static_cast(json[pos]))) pos++; unsigned long long value = 0; bool has_digit = false; while (pos < json.size() && std::isdigit(static_cast(json[pos]))) { has_digit = true; value = value * 10ULL + static_cast(json[pos] - '0'); pos++; } return has_digit ? value : fallback; } // 函数作用:生成统一同步状态响应,按版本判断是否返回完整数据。 std::string makeSyncStateResponse(const std::string& client, unsigned long long last_version) { // 构造统一同步响应。 // 客户端传入自己的 last_version,服务端比较全局 sync_version: // - 未变化:只返回 changed=false; // - 有变化:按 client 类型返回它需要的数据子集。 // 这样 K210 不需要下载考勤记录,STM32 不需要下载人脸照片,减少端侧负担。 const unsigned long long version = readSyncVersion(); const bool changed = last_version < version; updateDeviceSyncState(client.empty() ? "all" : client, version, 0); std::ostringstream json; json << "{\"version\":" << version << ",\"changed\":" << (changed ? "true" : "false") << ",\"client\":\"" << jsonEscape(client) << "\"" << ",\"server_time\":\"" << jsonEscape(getCurrentTimestamp()) << "\""; if (changed) { json << ",\"state\":{"; bool first = true; bool emitted_face_profiles = false; if (client == "k210" || client == "all" || client.empty()) { // K210 需要人脸绑定关系和待删除任务。 // face_profiles 用于把 K210 原始 face_id 映射成 student_id; // pending_delete/delete_tasks 用于远程删除 K210 本地特征和照片。 const std::string profiles = extractJsonMemberValue(readFaceProfiles(), "face_profiles", "[]"); const std::string pending_json = getPendingDeleteList(); const std::string pending = extractJsonMemberValue(pending_json, "pending_delete", "[]"); const std::string delete_tasks = extractJsonMemberValue(pending_json, "delete_tasks", "[]"); json << "\"face_profiles\":" << profiles << ",\"pending_delete\":" << pending << ",\"delete_tasks\":" << delete_tasks; first = false; emitted_face_profiles = true; } if (client == "stm32" || client == "qt" || client == "all" || client.empty()) { // STM32 和 Qt 都需要时间段配置。 // STM32 用于本地判断签到时段,Qt 用于管理端展示和编辑。 if (!first) json << ","; const std::string slots = extractJsonMemberValue(readTimeSlots(), "time_slots", "[]"); json << "\"time_slots\":" << slots; first = false; } if (client == "qt" || client == "all" || client.empty()) { // Qt 管理端需要完整状态,用于刷新页面和导出统计。 if (!first) json << ","; json << "\"students\":" << extractJsonMemberValue(readStudents(), "students", "[]") << ",\"classes\":" << extractJsonMemberValue(readClasses(), "classes", "[]"); if (!emitted_face_profiles) { json << ",\"face_profiles\":" << extractJsonMemberValue(readFaceProfiles(), "face_profiles", "[]"); } json << ",\"face_photos\":" << extractJsonMemberValue(getFacePhotosList(), "photos", "[]") << ",\"records\":" << readDatabase(1000, 0) << ",\"statistics\":" << getStatistics(); } json << "}"; } json << "}"; return json.str(); } // 函数作用:处理客户端同步确认请求并返回确认结果。 std::string makeSyncAckResponse(const std::string& request_body) { // 处理终端同步确认。 // K210 删除本地人脸后会上报 deleted 和 task_ids,服务端据此把 pending_delete 标记为 acked。 const std::string client = extractJsonStringValue(request_body, "client", "unknown"); const unsigned long long ack_version = static_cast(jsonNumberValue(request_body, "version", 0)); const std::vector deleted = extractJsonStringArray(request_body, "deleted"); const std::vector task_ids = extractJsonStringArray(request_body, "task_ids"); int confirmed_delete_count = 0; for (const std::string& task_id : task_ids) { // 优先按 task_id ACK,避免不同时间的同一 face_id 删除任务混淆。 if (!task_id.empty() && removeFromPendingDelete(task_id)) { confirmed_delete_count++; } } for (const std::string& id : deleted) { // 兼容旧版本 K210:如果只上报 face_id,也允许按 face_id ACK。 if (!id.empty() && removeFromPendingDelete(id)) { confirmed_delete_count++; } } updateDeviceSyncState(client, readSyncVersion(), ack_version); if (confirmed_delete_count > 0) { bumpSyncVersion("sync_ack_delete"); } std::ostringstream json; json << "{\"success\":true," << "\"client\":\"" << jsonEscape(client) << "\"," << "\"version\":" << readSyncVersion() << "," << "\"confirmed_delete_count\":" << confirmed_delete_count << "}"; return json.str(); } // 函数作用:生成当前同步版本号响应。 std::string makeSyncVersionResponse() { std::ostringstream json; json << "{" << "\"version\":" << readSyncVersion() << "," << "\"server_time\":\"" << jsonEscape(getCurrentTimestamp()) << "\"" << "}"; return json.str(); } // 函数作用:生成数据库和照片存储状态响应。 std::string makeStorageStatusResponse() { // 返回存储层状态,包括 quick_check、表行数、同步版本和各设备同步进度。 std::string quick_check_message; const bool quick_ok = sqliteQuickCheck(quick_check_message); sqlite3* db = openSqliteDb(); int records = 0; int face_profiles = 0; int pending_deletes = 0; int face_photos = countFacePhotoFiles(); if (db) { records = sqliteCountRows(db, "records"); face_profiles = sqliteCountRows(db, "face_profiles"); 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_profiles\":" << face_profiles << "," << "\"face_photos\":" << face_photos << "," << "\"pending_deletes\":" << pending_deletes << "," << "\"sync_version\":" << readSyncVersion() << "," << "\"device_sync_state\":" << getDeviceSyncSummary() << "," << "\"server_time\":\"" << jsonEscape(getCurrentTimestamp()) << "\"" << "}"; return json.str(); } // 函数作用:生成系统健康检查响应。 std::string makeHealthResponse() { // 健康检查接口,供管理端或脚本确认服务端是否可用。 std::string quick_check_message; const bool quick_ok = sqliteQuickCheck(quick_check_message); std::ostringstream json; json << "{" << "\"status\":\"" << (quick_ok ? "healthy" : "degraded") << "\"," << "\"time\":\"" << jsonEscape(getCurrentTimestamp()) << "\"," << "\"storage\":\"sqlite\"," << "\"database\":\"" << jsonEscape(SQLITE_DB_FILE) << "\"," << "\"database_ok\":" << (quick_ok ? "true" : "false") << "," << "\"quick_check\":\"" << jsonEscape(quick_check_message) << "\"," << "\"sync_version\":" << readSyncVersion() << "}"; return json.str(); } // ============= 检查重复签到 ============= // 参数: student_id - 学号, slot_start - 时段开始时间(HH:MM), slot_end - 时段结束时间(HH:MM) // 返回: JSON格式 {"duplicate": true/false, "record_time": "..."} // 函数作用:检查指定学生在时间段内是否已经存在签到记录。 std::string checkDuplicate(const std::string& student_id, const std::string& slot_start, const std::string& slot_end) { // 在指定时间段内检查某学生今天是否已签到。 // STM32 本地判断前可调用该接口,防止网络重试或重复刷脸造成重复记录。 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; } // ============= 人脸照片管理函数 ============= // 获取人脸照片列表 // 函数作用:读取人脸照片列表并生成 JSON 响应。 std::string getFacePhotosList() { // 返回服务端已保存的人脸照片列表。 // K210 启动时会读取该接口,判断本地照片哪些还需要补传。 std::ostringstream json; json << "{\"photos\":["; sqlite3* db = openSqliteDb(); bool first = true; if (db) { sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "SELECT face_id,file_path FROM face_photos ORDER BY face_id", -1, &stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { const std::string face_id = sqliteColumnText(stmt, 0); const std::string file_path = sqliteColumnText(stmt, 1); if (!isSafeId(face_id) || getFileSize(file_path) <= 0) { continue; } if (!first) json << ","; json << "{\"id\":\"" << jsonEscape(face_id) << "\",\"filename\":\"" << jsonEscape(face_id + ".jpg") << "\"}"; first = false; } } sqlite3_finalize(stmt); sqlite3_close(db); } json << "]}"; return json.str(); } // 保存人脸照片 // 函数作用:保存指定人脸编号对应的照片文件和数据库记录。 bool saveFacePhoto(const std::string& face_id, const std::string& photo_data) { // 保存 K210 上传的人脸照片。 // 文件落盘成功后同步写入 face_photos 索引,并确保 face_profiles 至少存在一条 active 档案。 _mkdir(FACES_DIR.c_str()); std::string filepath; if (!buildFacePhotoPath(face_id, filepath)) { std::cerr << "[FACE_PHOTO] Invalid face_id: " << face_id << std::endl; return false; } std::ofstream file(filepath, std::ios::binary); if (!file.is_open()) { std::cerr << "[FACE_PHOTO] Failed to create: " << filepath << std::endl; return false; } file.write(photo_data.c_str(), photo_data.size()); file.close(); sqlite3* db = openSqliteDb(); if (db) { const std::string now = getCurrentTimestamp(); // 如果该 face_id 曾经被加入待删除列表,但现在又上传了新照片, // 说明该人脸仍然有效,应取消旧删除任务,避免 K210 下次同步误删。 cancelPendingDeleteForFace(db, face_id, now); sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "REPLACE INTO face_photos(face_id,file_path,content_type,file_size,uploaded_at) VALUES(?,?,?,?,?)", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, face_id); sqliteBindText(stmt, 2, filepath); sqliteBindText(stmt, 3, "image/jpeg"); sqlite3_bind_int64(stmt, 4, static_cast(photo_data.size())); sqliteBindText(stmt, 5, now); sqlite3_step(stmt); } sqlite3_finalize(stmt); // student_id 为空表示此时只有人脸照片,还没有绑定学生。 upsertFaceProfile(db, face_id, "", "active", now); reconcileFaceProfilesWithPhotos(db, now); sqlite3_close(db); } std::cout << "[FACE_PHOTO] Saved: " << filepath << " (" << photo_data.size() << " bytes)" << std::endl; return true; } // 读取人脸照片 // 函数作用:读取指定人脸编号对应的照片二进制数据。 std::string readFacePhoto(const std::string& face_id) { // 按 face_id 读取 JPEG 二进制内容,直接作为 image/jpeg 响应返回。 std::string filepath; if (!buildFacePhotoPath(face_id, filepath)) return ""; std::ifstream file(filepath, std::ios::binary); if (!file.is_open()) return ""; std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); return content; } // 删除人脸照片 // 函数作用:删除指定人脸编号对应的照片文件和记录。 bool deleteFacePhoto(const std::string& face_id) { // 删除服务端照片文件和 face_photos 索引,并解除对应人脸档案绑定。 // 注意:这不会直接删除 K210 本地特征,K210 本地删除依赖 pending_delete 同步任务。 std::string filepath; if (!buildFacePhotoPath(face_id, filepath)) return false; sqlite3* db = openSqliteDb(); if (db) { sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "DELETE FROM face_photos WHERE face_id=?", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, face_id); sqlite3_step(stmt); } sqlite3_finalize(stmt); clearFaceProfileBinding(db, face_id, getCurrentTimestamp()); sqlite3_close(db); } if (std::remove(filepath.c_str()) == 0) { std::cout << "[FACE_PHOTO] Deleted: " << filepath << std::endl; return true; } return false; } // 删除指定学生ID的所有考勤记录 // 函数作用:删除指定学生关联的全部考勤记录并返回删除数量。 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; } // 函数作用:查询指定学生绑定的全部人脸编号。 std::vector findFaceIdsForStudent(sqlite3* db, const std::string& student_id) { // 通过 student_id 找到所有绑定的人脸编号。 // 删除接口既支持传 face_id,也支持传 student_id,因此需要这个反查能力。 std::vector face_ids; if (!db || student_id.empty()) return face_ids; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "SELECT face_id FROM face_profiles WHERE student_id=? AND face_id<>'' ORDER BY face_id", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, student_id); while (sqlite3_step(stmt) == SQLITE_ROW) { face_ids.push_back(sqliteColumnText(stmt, 0)); } } sqlite3_finalize(stmt); return face_ids; } // 函数作用:更新指定客户端设备的同步进度状态。 bool updateDeviceSyncState(const std::string& client, unsigned long long seen_version, unsigned long long ack_version) { // 记录客户端同步状态: // - last_seen_version:客户端最近拉取到的服务端版本; // - last_ack_version:客户端确认完成处理的版本; // - last_seen_at:最近一次访问时间。 if (client.empty()) return false; sqlite3* db = openSqliteDb(); if (!db) return false; sqlite3_stmt* stmt = nullptr; bool ok = false; const char* sql = "INSERT INTO device_sync_state(client,last_seen_version,last_ack_version,last_seen_at) " "VALUES(?,?,?,?) " "ON CONFLICT(client) DO UPDATE SET " "last_seen_version=MAX(device_sync_state.last_seen_version, excluded.last_seen_version)," "last_ack_version=MAX(device_sync_state.last_ack_version, excluded.last_ack_version)," "last_seen_at=excluded.last_seen_at"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, client); sqlite3_bind_int64(stmt, 2, static_cast(seen_version)); sqlite3_bind_int64(stmt, 3, static_cast(ack_version)); sqliteBindText(stmt, 4, getCurrentTimestamp()); ok = sqlite3_step(stmt) == SQLITE_DONE; } sqlite3_finalize(stmt); sqlite3_close(db); return ok; } // 函数作用:读取各客户端设备的同步状态摘要。 std::string getDeviceSyncSummary() { // 返回所有客户端同步状态,给 /api/v1/system/storage 展示。 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) { // 管理端删除人脸时,服务端无法直接操作 K210 SD 卡中的特征文件。 // 因此先写入 pending_delete,等待 K210 下次同步后在本地删除并 ACK。 sqlite3* db = openSqliteDb(); if (!db) return false; sqlite3_stmt* stmt = nullptr; bool ok = false; std::string task_id = face_id + "-" + std::to_string(static_cast(time(0))); if (sqlite3_prepare_v2(db, "REPLACE INTO pending_delete(task_id,face_id,student_id,created_at,status) VALUES(?,?,?,?,?)", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, task_id); sqliteBindText(stmt, 2, face_id); sqliteBindText(stmt, 3, student_id); sqliteBindText(stmt, 4, getCurrentTimestamp()); sqliteBindText(stmt, 5, "pending"); ok = sqlite3_step(stmt) == SQLITE_DONE; } sqlite3_finalize(stmt); sqlite3_close(db); std::cout << "[PENDING_DELETE] Added: " << face_id << " task=" << task_id << std::endl; return ok; } // 获取待删除列表 // 函数作用:读取待删除人脸任务列表并生成 JSON 响应。 std::string getPendingDeleteList() { // 同时返回两种格式: // - pending_delete:旧版本兼容的 face_id 字符串数组; // - delete_tasks:包含 task_id 的新格式,ACK 更精确。 sqlite3* db = openSqliteDb(); if (!db) return "{\"pending_delete\":[],\"delete_tasks\":[]}"; sqlite3_stmt* stmt = nullptr; std::vector> tasks; if (sqlite3_prepare_v2(db, "SELECT task_id,face_id,student_id,created_at,status,COALESCE(ack_at,'') FROM pending_delete WHERE status<>'acked' ORDER BY created_at,face_id", -1, &stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { std::map row; row["task_id"] = sqliteColumnText(stmt, 0); row["face_id"] = sqliteColumnText(stmt, 1); row["student_id"] = sqliteColumnText(stmt, 2); row["created_at"] = sqliteColumnText(stmt, 3); row["status"] = sqliteColumnText(stmt, 4); row["ack_at"] = sqliteColumnText(stmt, 5); tasks.push_back(row); } } sqlite3_finalize(stmt); sqlite3_close(db); std::ostringstream json; json << "{\"pending_delete\":["; bool first = true; for (const auto& task : tasks) { if (!first) json << ","; json << "\"" << jsonEscape(task.at("face_id")) << "\""; first = false; } json << "],\"delete_tasks\":["; first = true; for (const auto& task : tasks) { if (!first) json << ","; json << "{" << "\"task_id\":\"" << jsonEscape(task.at("task_id")) << "\"," << "\"face_id\":\"" << jsonEscape(task.at("face_id")) << "\"," << "\"student_id\":\"" << jsonEscape(task.at("student_id")) << "\"," << "\"created_at\":\"" << jsonEscape(task.at("created_at")) << "\"," << "\"status\":\"" << jsonEscape(task.at("status")) << "\"" << "}"; first = false; } json << "]}"; return json.str(); } // 从待删除列表中移除指定ID(K210确认删除后调用) // 函数作用:从待删除队列中移除指定任务或人脸编号。 bool removeFromPendingDelete(const std::string& face_id_or_task_id) { // K210 删除本地特征后调用 /api/v1/sync/ack。 // 服务端不物理删除任务,而是标记为 acked,保留审计线索。 sqlite3* db = openSqliteDb(); if (!db) return false; sqlite3_stmt* stmt = nullptr; bool ok = false; if (sqlite3_prepare_v2(db, "UPDATE pending_delete SET status='acked',ack_at=? WHERE (task_id=? OR face_id=?) AND status<>'acked'", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, getCurrentTimestamp()); sqliteBindText(stmt, 2, face_id_or_task_id); sqliteBindText(stmt, 3, face_id_or_task_id); ok = sqlite3_step(stmt) == SQLITE_DONE && sqlite3_changes(db) > 0; } sqlite3_finalize(stmt); sqlite3_close(db); std::cout << "[PENDING_DELETE] Acked: " << face_id_or_task_id << std::endl; return ok; } // 函数作用:取消指定人脸编号对应的待删除任务。 bool cancelPendingDeleteForFace(sqlite3* db, const std::string& face_id, const std::string& now) { // 某 face_id 重新上传照片或重新绑定时,说明它不应该再被删除。 // 将未确认删除任务标记为 acked,防止终端误删。 if (!db || !isSafeId(face_id)) return false; sqlite3_stmt* stmt = nullptr; bool ok = false; if (sqlite3_prepare_v2(db, "UPDATE pending_delete SET status='acked',ack_at=? WHERE face_id=? AND status<>'acked'", -1, &stmt, nullptr) == SQLITE_OK) { sqliteBindText(stmt, 1, now); sqliteBindText(stmt, 2, face_id); ok = sqlite3_step(stmt) == SQLITE_DONE; } const int changed = sqlite3_changes(db); sqlite3_finalize(stmt); if (ok && changed > 0) { std::cout << "[PENDING_DELETE] Canceled stale delete for face_id: " << face_id << std::endl; } return ok; } // ============= K210同步请求管理 ============= // 设置同步请求标志(QT端调用) // 函数作用:设置是否请求 K210 立即执行同步。 bool setSyncRequest(bool sync) { // sync_request 是给 K210 轮询的轻量标志。 // 管理端保存人脸档案或删除人脸后置为 true,K210 看到后再拉取统一同步状态。 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轮询调用) // 函数作用:读取当前同步请求标志并生成 JSON 响应。 std::string getSyncRequest() { // K210 定期 GET /api/v1/sync/request,根据返回值决定是否触发完整同步。 return sqliteGetKvValue("sync_request", "{\"sync\":false}"); } // 清除同步请求标志(K210确认后调用) // 函数作用:清除当前同步请求标志。 bool clearSyncRequest() { // K210 收到同步请求后会先 ACK 该标志,随后执行 sync/state。 return setSyncRequest(false); } // ============= HTTP请求处理 ============= // 函数作用:处理单个客户端 HTTP 连接并分发到具体 API 逻辑。 void handleHttpRequest(SOCKET client_socket) { // 每个 TCP 连接由一个独立线程处理。 // 这里手工完成 HTTP 解析、鉴权、请求体读取、解密、路由分发和响应拼接。 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 endpoint = path; std::string query; size_t q_pos = path.find('?'); if (q_pos != std::string::npos) { endpoint = path.substr(0, q_pos); query = path.substr(q_pos + 1); } std::string response_body; std::string content_type = "application/json"; int status_code = 200; std::string status_text = "OK"; auto sendResponse = [&]() { // 统一响应出口。 // 非公开 /api/v1 JSON 响应默认加密,图片等二进制响应保持明文原始格式。 bool response_encrypted = false; const bool can_encrypt_response = method != "OPTIONS" && endpoint.find("/api/v1/") == 0 && !isPublicEndpoint(method, endpoint) && !response_body.empty() && isJsonLikeContentType(content_type); if (can_encrypt_response) { // 加密失败时返回加密后的错误 JSON;如果连错误 JSON 都无法加密,则按 500 处理。 const std::string encrypted_body = encryptPayload(response_body); if (encrypted_body.empty()) { status_code = 500; status_text = "Internal Server Error"; response_body = makeJsonError("Encryption failed"); content_type = "application/json"; } else { response_body = encrypted_body; content_type = "application/octet-stream"; response_encrypted = true; } } if (can_encrypt_response && !response_encrypted) { const std::string fallback_body = encryptPayload(makeJsonError("Encryption failed")); if (!fallback_body.empty()) { response_body = fallback_body; content_type = "application/octet-stream"; response_encrypted = true; } } std::ostringstream response; // 手工拼接 HTTP 响应头。 // Content-Length 必须使用加密/明文转换后的最终 body 长度。 response << "HTTP/1.1 " << status_code << " " << status_text << "\r\n"; response << "Content-Type: " << content_type; if (isJsonLikeContentType(content_type)) { response << "; charset=utf-8"; } response << "\r\n"; if (response_encrypted) { response << "X-Encrypted: AES128-CBC\r\n"; } response << "Content-Length: " << response_body.length() << "\r\n"; response << "Access-Control-Allow-Origin: " << allowedCorsOrigin() << "\r\n"; response << "Access-Control-Allow-Headers: Content-Type, Authorization, X-Api-Token, X-Encrypted-Response, X-Encrypted\r\n"; 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); }; if (method != "OPTIONS" && endpoint.find("/api/v1/") != 0) { // 除 OPTIONS 外,只接受 /api/v1/ 前缀的业务接口。 response_body = "{\"error\":\"Not Found\"}"; status_code = 404; status_text = "Not Found"; sendResponse(); return; } if (!isAuthorizedRequest(request, method, endpoint)) { // 所有业务接口都需要 API Token,防止局域网中未授权设备调用写接口。 response_body = makeJsonError("Unauthorized"); status_code = 401; status_text = "Unauthorized"; sendResponse(); return; } // 提取请求体。 // 首次 recv 可能只读到部分 body,因此需要根据 Content-Length 继续补读。 std::string request_body; if (method == "POST") { size_t content_length = 0; std::string cl_str; if (getHeaderValue(request, "Content-Length", cl_str)) { int parsed_content_length = 0; if (!parseIntInRange(cl_str, 0, static_cast(MAX_ENCRYPTED_PHOTO_BODY_SIZE), parsed_content_length)) { response_body = makeJsonError("Invalid Content-Length"); status_code = 400; status_text = "Bad Request"; sendResponse(); return; } content_length = static_cast(parsed_content_length); } const bool is_photo_upload = endpoint.find("/api/v1/faces/photos/") == 0; const bool encrypted_body = isEncryptedRequest(request); // 照片和 JSON 使用不同大小限制;加密 body 因 Base64 编码会更大。 const size_t max_body_size = encrypted_body ? (is_photo_upload ? MAX_ENCRYPTED_PHOTO_BODY_SIZE : MAX_ENCRYPTED_JSON_BODY_SIZE) : (is_photo_upload ? MAX_PHOTO_BODY_SIZE : MAX_JSON_BODY_SIZE); if (content_length > max_body_size) { response_body = makeJsonError("Request body too large"); status_code = 413; status_text = "Payload Too Large"; sendResponse(); return; } size_t header_end = request.find("\r\n\r\n"); if (header_end != std::string::npos) { size_t body_start = header_end + 4; size_t current_body_size = request.size() - body_start; if (current_body_size >= content_length) { request_body = request.substr(body_start, content_length); } else { request_body = request.substr(body_start); size_t remaining = content_length - current_body_size; while (remaining > 0) { char extra_buffer[BUFFER_SIZE] = {0}; int extra_bytes = recv(client_socket, extra_buffer, static_cast(std::min(remaining, (size_t)BUFFER_SIZE)), 0); if (extra_bytes <= 0) break; request_body.append(extra_buffer, extra_bytes); remaining -= extra_bytes; } } } std::cout << "[HTTP] POST body size: " << request_body.size() << " bytes (expected: " << content_length << ")" << std::endl; if (request_body.size() != content_length) { response_body = makeJsonError("Incomplete request body"); status_code = 400; status_text = "Bad Request"; sendResponse(); return; } if (!is_photo_upload && !encrypted_body) { // 除照片上传外,POST JSON 必须加密。 // 这样学生档案、人脸绑定、同步 ACK 等管理数据不会明文传输。 response_body = makeJsonError("Encrypted request body required"); status_code = 400; status_text = "Bad Request"; sendResponse(); return; } if (encrypted_body) { // 客户端声明加密后,先解密再进入具体路由处理。 const std::string decrypted_body = decryptPayload(request_body); if (decrypted_body.empty() && content_length > 0) { response_body = makeJsonError("Decrypt request body failed"); status_code = 400; status_text = "Bad Request"; sendResponse(); return; } if (decrypted_body.size() > (is_photo_upload ? MAX_PHOTO_BODY_SIZE : MAX_JSON_BODY_SIZE)) { response_body = makeJsonError("Decrypted request body too large"); status_code = 413; status_text = "Payload Too Large"; sendResponse(); return; } request_body = decrypted_body; std::cout << "[HTTP] Decrypted POST body size: " << request_body.size() << " bytes" << std::endl; } } // ============= 路由处理 ============= // 路由按 method + endpoint 匹配,所有响应都写入 response_body,最后统一 sendResponse。 if (method == "OPTIONS") { // 浏览器 CORS 预检请求。 response_body = "{}"; } else if (method == "GET" && endpoint == "/api/v1/sync/version") { // 查询当前服务端同步版本。 response_body = makeSyncVersionResponse(); std::cout << "[SYNC] Version fetched: " << readSyncVersion() << std::endl; } else if (method == "GET" && endpoint == "/api/v1/sync/state") { // 统一同步接口:K210/STM32/Qt 根据 client 参数获取各自需要的状态。 auto params = parseQueryParams(query); const std::string client = params.count("client") ? params["client"] : "all"; const unsigned long long last_version = parseVersionParam(params); response_body = makeSyncStateResponse(client, last_version); std::cout << "[SYNC] State fetched: client=" << client << ", last_version=" << last_version << ", current_version=" << readSyncVersion() << std::endl; } else if (method == "POST" && endpoint == "/api/v1/sync/ack") { // 终端确认已处理某个同步版本或删除任务。 response_body = makeSyncAckResponse(request_body); std::cout << "[SYNC] Ack received: " << request_body << std::endl; } else if (method == "POST" && endpoint == "/api/v1/attendance/checkins") { // STM32 上传签到记录。 // 请求体应包含 student_id、name、confidence、status、class_id 等字段。 std::cout << "[CHECKIN] Processing checkin data..." << std::endl; auto json_data = parseJSON(request_body); if (json_data.find("student_id") != json_data.end()) { std::string timestamp = getCurrentTimestamp(); std::string student_id = json_data["student_id"]; std::string name = json_data.count("name") ? json_data["name"] : ""; std::string confidence = json_data.count("confidence") ? json_data["confidence"] : "0"; std::string status = json_data.count("status") ? json_data["status"] : "success"; std::string class_id = json_data.count("class_id") ? json_data["class_id"] : "0"; char* conf_end = nullptr; const double conf_value = std::strtod(confidence.c_str(), &conf_end); if (!isSafeId(student_id) || student_id.size() > 32 || name.size() > 64 || class_id.size() > 32 || conf_end == confidence.c_str() || *conf_end != '\0' || conf_value < 0.0 || conf_value > 1.0 || (status != "success" && status != "failed" && status != "late")) { // 字段合法性校验,防止异常终端数据污染数据库。 response_body = makeJsonError("Invalid checkin fields"); status_code = 400; status_text = "Bad Request"; sendResponse(); return; } std::cout << "[CHECKIN] Student ID: " << student_id << std::endl; if (saveAttendanceRecord(timestamp, student_id, name, confidence, status, class_id)) { // 签到记录写入成功后推进版本号,管理端统计和记录列表可以感知变化。 response_body = "{\"success\":true,\"message\":\"签到成功\",\"timestamp\":\"" + jsonEscape(timestamp) + "\"}"; bumpSyncVersion("checkin"); std::cout << "[CHECKIN] ✓ " << name << " (ID:" << student_id << ", Class:" << class_id << ")" << std::endl; } else { 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 if (method == "GET" && endpoint == "/api/v1/attendance/records") { // 管理端分页获取考勤记录。 auto params = parseQueryParams(query); int limit = 100; int offset = 0; if (params.count("limit") && !parseIntInRange(params["limit"], 1, 5000, limit)) { response_body = makeJsonError("Invalid limit"); status_code = 400; status_text = "Bad Request"; } else if (params.count("offset") && !parseIntInRange(params["offset"], 0, 1000000, offset)) { response_body = makeJsonError("Invalid offset"); status_code = 400; status_text = "Bad Request"; } else { response_body = readDatabase(limit, offset); } } else if (method == "GET" && endpoint == "/api/v1/attendance/statistics") { // 管理端获取统计数据。 response_body = getStatistics(); } else if (method == "POST" && endpoint == "/api/v1/attendance/records/clear") { // 清空考勤记录,通常用于测试或重新开始采集。 if (clearDatabase()) { response_body = "{\"success\":true,\"message\":\"Database cleared\"}"; bumpSyncVersion("clear_records"); } else { response_body = "{\"success\":false,\"message\":\"Failed to clear database\"}"; status_code = 500; status_text = "Internal Server Error"; } } else if (method == "DELETE" && endpoint == "/api/v1/attendance/records") { // 删除单条考勤记录。 auto params = parseQueryParams(query); if (params.count("timestamp") && params.count("student_id")) { std::string timestamp = params["timestamp"]; std::string student_id = params["student_id"]; if (deleteRecord(timestamp, student_id)) { response_body = "{\"success\":true,\"message\":\"Record deleted\"}"; bumpSyncVersion("delete_record"); } else { response_body = "{\"success\":false,\"message\":\"Record not found\"}"; status_code = 404; status_text = "Not Found"; } } else { response_body = "{\"success\":false,\"message\":\"Missing parameters\"}"; status_code = 400; status_text = "Bad Request"; } } else if (method == "GET" && endpoint == "/api/v1/system/health") { // 健康检查。 response_body = makeHealthResponse(); } else if (method == "GET" && endpoint == "/api/v1/system/storage") { // 存储状态和同步状态检查。 response_body = makeStorageStatusResponse(); } else if (method == "GET" && endpoint == "/api/v1/system/time") { // 返回服务器当前Unix时间戳(UTC秒)和可读时间 std::ostringstream json; json << "{" << "\"epoch\":" << static_cast(time(0)) << "," << "\"server_time\":\"" << getCurrentTimestamp() << "\"" << "}"; response_body = json.str(); } else if (method == "POST" && endpoint == "/api/v1/config/time-slots") { // 上传时间段配置: POST /api/v1/config/time-slots std::cout << "[TIMESLOTS] Received upload request, data size: " << request_body.size() << " bytes" << std::endl; if (saveTimeSlots(request_body)) { response_body = "{\"success\":true,\"message\":\"时间段配置已更新\"}"; bumpSyncVersion("timeslots"); std::cout << "[TIMESLOTS] ✓ Configuration uploaded successfully" << std::endl; } else { response_body = "{\"success\":false,\"message\":\"保存时间段配置失败\"}"; status_code = 500; status_text = "Internal Server Error"; } } else if (method == "GET" && endpoint == "/api/v1/config/time-slots") { // 获取时间段配置: GET /api/v1/config/time-slots std::cout << "[TIMESLOTS] Reading configuration" << std::endl; response_body = readTimeSlots(); } else if (method == "GET" && endpoint == "/api/v1/attendance/duplicates") { // 检查重复签到: GET /api/v1/attendance/duplicates?student_id=xxx&slot_start=08:00&slot_end=09:30 auto params = parseQueryParams(query); if (params.count("student_id") && params.count("slot_start") && params.count("slot_end")) { response_body = checkDuplicate(params["student_id"], params["slot_start"], params["slot_end"]); std::cout << "[CHECK] student_id=" << params["student_id"] << ", slot=" << params["slot_start"] << "-" << params["slot_end"] << ", result=" << response_body << std::endl; } else { response_body = "{\"error\":\"Missing parameters: student_id, slot_start, slot_end\"}"; status_code = 400; status_text = "Bad Request"; } } else if (method == "GET" && endpoint == "/api/v1/attendance/today") { // 获取今日已签到记录(供STM32开机下载) // GET /api/v1/attendance/today?slot_start=08:00&slot_end=09:30 auto params = parseQueryParams(query); std::string slot_start = params.count("slot_start") ? params["slot_start"] : "00:00"; std::string slot_end = params.count("slot_end") ? params["slot_end"] : "23:59"; std::string todayDate = getCurrentTimestamp().substr(0, 10); int start_hour = 0, start_minute = 0, end_hour = 23, end_minute = 59; // 若传入时间格式不合法,parseTimeParts 会保留默认 00:00~23: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 == "GET" && endpoint == "/api/v1/students") { response_body = readStudents(); std::cout << "[STUDENTS] List fetched" << std::endl; } else if (method == "POST" && endpoint == "/api/v1/students") { if (saveStudents(request_body)) { response_body = "{\"success\":true,\"message\":\"学生档案已保存\"}"; bumpSyncVersion("students"); } else { response_body = "{\"success\":false,\"message\":\"保存学生档案失败\"}"; status_code = 400; status_text = "Bad Request"; } } else if (method == "GET" && endpoint == "/api/v1/classes") { response_body = readClasses(); std::cout << "[CLASSES] List fetched" << std::endl; } else if (method == "POST" && endpoint == "/api/v1/classes") { if (saveClasses(request_body)) { response_body = "{\"success\":true,\"message\":\"班级配置已保存\"}"; bumpSyncVersion("classes"); } else { response_body = "{\"success\":false,\"message\":\"保存班级失败,请确认被删除班级下没有学生\"}"; status_code = 400; status_text = "Bad Request"; } } else if (method == "GET" && endpoint == "/api/v1/face_profiles") { response_body = readFaceProfiles(); std::cout << "[FACE_PROFILE] List fetched" << std::endl; } else if (method == "POST" && endpoint == "/api/v1/face_profiles") { // 管理端保存人脸和学生绑定关系。 // 成功后不仅推进版本号,还设置 sync_request=true,提醒 K210 尽快同步。 if (saveFaceProfiles(request_body)) { response_body = "{\"success\":true,\"message\":\"人脸档案已保存,已触发 K210 同步\"}"; bumpSyncVersion("face_profiles"); setSyncRequest(true); } else { response_body = "{\"success\":false,\"message\":\"保存人脸档案失败\"}"; status_code = 400; status_text = "Bad Request"; } // ============= 人脸照片管理 API ============= } else if (method == "GET" && endpoint == "/api/v1/faces/photos") { // 获取人脸照片列表 _mkdir(FACES_DIR.c_str()); response_body = getFacePhotosList(); std::cout << "[FACE_PHOTO] Photos list fetched" << std::endl; } else if (method == "GET" && endpoint.find("/api/v1/faces/photos/") == 0) { // 获取指定人脸照片: GET /api/v1/faces/photos/ std::string face_id = urlDecode(endpoint.substr(std::string("/api/v1/faces/photos/").size())); std::string photo_data = readFacePhoto(face_id); if (!photo_data.empty()) { response_body = photo_data; content_type = "image/jpeg"; std::cout << "[FACE_PHOTO] Photo fetched for id: " << face_id << std::endl; } else { response_body = "{\"error\":\"Photo not found\"}"; status_code = 404; status_text = "Not Found"; } } else if (method == "POST" && endpoint.find("/api/v1/faces/photos/") == 0) { // 人脸照片上传: POST /api/v1/faces/photos/ // 该接口主要由 K210 调用,body 是 JPEG 二进制数据,通常不做 AES 加密以降低端侧负担。 std::string face_id = urlDecode(endpoint.substr(std::string("/api/v1/faces/photos/").size())); std::cout << "[FACE_PHOTO] Simple upload for face_id: " << face_id << ", size: " << request_body.size() << " bytes" << std::endl; if (!isSafeId(face_id)) { response_body = "{\"success\":false,\"message\":\"Invalid face_id\"}"; status_code = 400; status_text = "Bad Request"; } else if (!request_body.empty()) { if (saveFacePhoto(face_id, request_body)) { response_body = "{\"success\":true,\"message\":\"Photo saved\",\"face_id\":\"" + jsonEscape(face_id) + "\"}"; bumpSyncVersion("face_photo"); } else { response_body = "{\"success\":false,\"message\":\"Failed to save photo\"}"; status_code = 500; status_text = "Internal Server Error"; } } else { response_body = "{\"success\":false,\"message\":\"Invalid face_id or empty data\"}"; status_code = 400; status_text = "Bad Request"; } } else if (method == "DELETE" && endpoint.find("/api/v1/faces/") == 0) { // 删除人脸信息: DELETE /api/v1/faces/?delete_records=true // 删除流程分两段: // 1. 服务端删除照片/解除绑定,并写入 pending_delete; // 2. K210 下次同步后删除本地特征,再通过 sync/ack 确认。 std::string requested_id = urlDecode(endpoint.substr(std::string("/api/v1/faces/").size())); auto params = parseQueryParams(query); bool delete_records = (params.count("delete_records") && params["delete_records"] == "true"); std::cout << "[DELETE_FACE] Deleting face for id: " << requested_id << ", delete_records: " << (delete_records ? "yes" : "no") << std::endl; if (!isSafeId(requested_id)) { response_body = "{\"success\":false,\"message\":\"Invalid id\"}"; status_code = 400; status_text = "Bad Request"; } else { std::vector face_ids; sqlite3* db = openSqliteDb(); if (db) { if (facePhotoExists(db, requested_id) || faceProfileExists(db, requested_id)) { face_ids.push_back(requested_id); } else { face_ids = findFaceIdsForStudent(db, requested_id); } sqlite3_close(db); } if (face_ids.empty()) { face_ids.push_back(requested_id); } bool photo_deleted = false; int records_deleted = 0; if (delete_records) { // 可选联动删除该学生历史考勤记录。 records_deleted = deleteRecordsByStudentId(requested_id); } // 添加K210原始face_id到待删除列表,等待K210同步删除本地特征 bool pending_added = false; for (const auto& face_id : face_ids) { photo_deleted = deleteFacePhoto(face_id) || photo_deleted; pending_added = addToPendingDelete(face_id, requested_id) || pending_added; } bumpSyncVersion("delete_face"); setSyncRequest(true); std::ostringstream result; result << "{\"success\":true," << "\"photo_deleted\":" << (photo_deleted ? "true" : "false") << "," << "\"profile_unbound\":true," << "\"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; } // ============= K210同步请求 API ============= } else if (method == "GET" && endpoint == "/api/v1/sync/request") { // K210查询是否需要同步 response_body = getSyncRequest(); std::cout << "[SYNC] K210 checking sync request: " << response_body << std::endl; } else if (method == "POST" && endpoint == "/api/v1/sync/request/ack") { // K210确认收到同步请求,清除标志 if (clearSyncRequest()) { response_body = "{\"success\":true,\"message\":\"Sync acknowledged\"}"; std::cout << "[SYNC] K210 acknowledged sync request" << std::endl; } else { response_body = "{\"success\":false,\"message\":\"Failed to clear sync request\"}"; status_code = 500; status_text = "Internal Server Error"; } } else { response_body = "{\"error\":\"Not Found\"}"; status_code = 404; status_text = "Not Found"; } sendResponse(); } // ============= HTTP服务器线程 ============= // 函数作用:启动 HTTP 监听循环并为每个客户端创建处理线程。 void httpServerThread() { // 创建 TCP 监听 socket。 // 本服务采用一个连接一个线程的简单模型,代码清晰,适合本系统的小并发场景。 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; // 允许服务重启后快速复用端口,避免 TIME_WAIT 导致 bind 失败。 setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt)); sockaddr_in server_addr; server_addr.sin_family = AF_INET; const std::string bind_host = bindAddress(); if (bind_host == "0.0.0.0" || bind_host.empty()) { // 监听所有网卡,便于局域网内 K210、STM32、Qt 管理端访问。 server_addr.sin_addr.s_addr = INADDR_ANY; } else { // 指定监听地址时做格式校验,例如 127.0.0.1 或服务器内网 IP。 if (inet_pton(AF_INET, bind_host.c_str(), &server_addr.sin_addr) != 1) { std::cerr << "[HTTP] Invalid ATTENDANCE_BIND_HOST: " << bind_host << std::endl; closesocket(server_socket); return; } } server_addr.sin_port = htons(HTTP_PORT); if (bind(server_socket, (sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) { std::cerr << "[HTTP] Bind failed" << std::endl; closesocket(server_socket); return; } listen(server_socket, 10); std::cout << "[HTTP] Server listening on " << bind_host << ":" << HTTP_PORT << std::endl; while (true) { // 阻塞等待客户端连接,收到连接后立即交给独立线程处理。 // detach 后主监听线程继续 accept,适合 K210 照片上传和管理端查询同时发生的场景。 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(); } } } // 函数作用:服务端程序入口,初始化存储并启动 HTTP 服务。 int main() { #ifdef _WIN32 // Windows 下使用 Winsock 前必须初始化 WSA。 WSADATA wsa; WSAStartup(MAKEWORD(2, 2), &wsa); #endif std::cout << "========================================" << std::endl; std::cout << " Face Recognition Attendance Server" << std::endl; std::cout << " with AES-128-CBC Encryption" << std::endl; std::cout << "========================================" << std::endl; std::cout << "HTTP Port " << HTTP_PORT << " - Encrypted API" << std::endl; std::cout << "Database: " << SQLITE_DB_FILE << std::endl; std::cout << "----------------------------------------" << std::endl; if (!backupSqliteDatabaseOnStartup()) { // 备份失败时直接停止,避免在不确定状态下继续写数据库。 std::cerr << "[BACKUP] Startup backup failed, stop to avoid unsafe writes" << std::endl; return 1; } // 初始化 SQLite,所有运行时数据统一写入 attendance.db。 if (!initializeSqliteStorage()) { std::cerr << "[SQLITE] Failed to initialize storage" << std::endl; return 1; } std::cout << "[SYNC] Current version: " << readSyncVersion() << std::endl; // 启动 HTTP 服务器。主线程 join,服务进程会一直运行直到被外部终止。 std::thread http_thread(httpServerThread); http_thread.join(); #ifdef _WIN32 WSACleanup(); #endif return 0; }