From a5da43cdf53cd4055032d98194d8f3daab5a3c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BA=AA=E7=94=B0=E9=87=8E?= Date: Thu, 21 May 2026 08:06:55 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=88=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- regression_api_tests.ps1 | 1 + server_encrypted.cpp | 467 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 432 insertions(+), 36 deletions(-) diff --git a/regression_api_tests.ps1 b/regression_api_tests.ps1 index e602a83..a25c956 100644 --- a/regression_api_tests.ps1 +++ b/regression_api_tests.ps1 @@ -9,6 +9,7 @@ $headers = @{ "Accept" = "application/json" } +# 函数作用:按给定名称、路径、HTTP方法和可选请求体调用服务器接口,并在控制台输出回归检查结果。 function Invoke-ApiCheck { param( [string]$Name, diff --git a/server_encrypted.cpp b/server_encrypted.cpp index 82a713d..edead02 100644 --- a/server_encrypted.cpp +++ b/server_encrypted.cpp @@ -1,10 +1,25 @@ /* - * 增强版考勤服务器 - 支持AES加密数据 + * 增强版考勤服务器 - 支持 AES 加密数据 * - * 功能: - * 1. HTTP端口8082 - 接收STM32加密签到数据 - * 2. HTTP API - 提供Qt客户端接口 - * 3. AES-128-CBC解密支持 + * 本文件是智慧课堂考勤系统的服务端主程序,采用“单文件 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 */ @@ -50,22 +65,36 @@ #include #include -const int HTTP_PORT = 8082; -const int BUFFER_SIZE = 4096; -const std::string SQLITE_DB_FILE = "attendance.db"; -const std::string LOG_FILE = "attendance.log"; -const std::string FACES_DIR = "faces"; // 人脸照片目录 -const std::string DB_BACKUP_DIR = "backups"; +const 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); @@ -73,17 +102,25 @@ std::string getEnvOrDefault(const char* name, const char* fallback) { 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; } @@ -109,7 +146,10 @@ 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) { @@ -133,7 +173,10 @@ std::string jsonEscape(const std::string& value) { 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') { @@ -157,7 +200,10 @@ std::string csvEscape(const std::string& value) { 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; } @@ -175,7 +221,10 @@ bool parseIntInRange(const std::string& value, int min_value, int max_value, int } } +// 函数作用:解析 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])) @@ -194,7 +243,10 @@ bool parseTimeParts(const std::string& value, int& hour, int& minute) { return true; } +// 函数作用:校验编号字符串是否只包含允许用于路径和主键的安全字符。 bool isSafeId(const std::string& value) { + // ID 安全校验:只允许字母、数字、下划线和短横线。 + // 该函数用于 student_id、class_id、face_id 等字段,尤其防止照片路径穿越。 if (value.empty() || value.size() > 64) { return false; } @@ -206,7 +258,10 @@ bool isSafeId(const std::string& value) { 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; } @@ -214,12 +269,16 @@ bool buildFacePhotoPath(const std::string& face_id, std::string& filepath) { 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); @@ -267,7 +326,10 @@ bool getHeaderValue(const std::string& request, const std::string& header_name, 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); @@ -309,7 +371,10 @@ bool hasHeaderToken(const std::string& request, const std::string& header_name, 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; @@ -330,27 +395,40 @@ bool hasBearerToken(const std::string& request, const std::string& expected) { 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)); }); @@ -359,7 +437,11 @@ bool isJsonLikeContentType(const std::string& content_type) { || lower.find("+json") != std::string::npos; } -// ⚠️ 重要:密钥和IV必须与STM32端完全一致! +// 重要:密钥和 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 @@ -371,7 +453,10 @@ static const unsigned char MY_AES_IV[16] = { }; // ============= 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) { @@ -398,7 +483,10 @@ std::string base64_decode(const std::string& encoded_string) { } // ============= 添加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++) { @@ -408,19 +496,20 @@ std::string add_pkcs7_padding(const std::string& data) { } // ============= 手动移除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; @@ -428,13 +517,16 @@ std::string remove_pkcs7_padding(const std::string& 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) { - // 添加PKCS#7填充 + // 响应加密流程: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(); @@ -443,17 +535,17 @@ std::string aes_encrypt_cbc(const std::string& plaintext) { 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; @@ -467,7 +559,7 @@ std::string aes_encrypt_cbc(const std::string& plaintext) { } 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); @@ -481,7 +573,10 @@ std::string aes_encrypt_cbc(const std::string& plaintext) { } // ============= 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 ""; @@ -493,17 +588,17 @@ std::string aes_decrypt_cbc(const std::string& encrypted_data) { return ""; } - // 初始化解密(使用标准AES-128-CBC) + // 初始化解密(使用标准 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 + // 禁用自动填充,我们手动处理 PKCS#7,保持两端行为一致。 EVP_CIPHER_CTX_set_padding(ctx, 0); - // 解密数据 + // 解密数据。 std::vector plaintext(encrypted_data.length() + 16); int len; int plaintext_len = 0; @@ -517,7 +612,7 @@ std::string aes_decrypt_cbc(const std::string& encrypted_data) { } 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); @@ -529,14 +624,16 @@ std::string aes_decrypt_cbc(const std::string& encrypted_data) { std::string result((char*)plaintext.data(), plaintext_len); - // 手动移除PKCS#7填充 + // 手动移除 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]); @@ -546,7 +643,9 @@ void print_hex(const std::string& label, const unsigned char* data, size_t len) } // ============= Base64编码 ============= +// 函数作用:将原始二进制字符串编码为 Base64 字符串。 std::string base64_encode(const std::string& input) { + // 将 AES 二进制密文编码成无换行 Base64 文本,便于作为 HTTP body 传输。 BIO *bio, *b64; BUF_MEM *bufferPtr; @@ -566,43 +665,50 @@ std::string base64_encode(const std::string& input) { } // ============= 加密传输数据 ============= +// 函数作用:加密通用响应或请求载荷并输出 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加密 + // AES 加密。 std::string encrypted = aes_encrypt_cbc(plain_data); if (encrypted.empty()) { std::cerr << "[CRYPTO] Encryption failed!" << std::endl; return ""; } - // Base64编码 + // 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解码 + // 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 + // 调试:打印密钥和 IV,便于排查端侧密钥不一致问题。 print_hex("[CRYPTO] Using KEY", MY_AES_KEY, 16); print_hex("[CRYPTO] Using IV", MY_AES_IV, 16); - // 2. AES解密 + // 2. AES 解密。 std::string decrypted = aes_decrypt_cbc(encrypted_binary); if (decrypted.empty()) { @@ -622,12 +728,16 @@ std::string decryptPayload(const std::string& encrypted_base64) { } } +// 函数作用:解密 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 @@ -641,7 +751,9 @@ std::string getCurrentTimestamp() { } // 清理字符串中的控制字符(\r, \n等) +// 函数作用:清理字符串首尾空白和控制字符。 std::string cleanString(const std::string& str) { + // 过滤串口或 HTTP 输入中可能混入的控制字符,避免写入 JSON/CSV 时破坏格式。 std::string result; result.reserve(str.length()); for (char c : str) { @@ -653,7 +765,10 @@ std::string cleanString(const std::string& str) { 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()) { @@ -674,7 +789,9 @@ std::string urlDecode(const std::string& str) { 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; @@ -690,7 +807,10 @@ std::map parseQueryParams(const std::string& query) { 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(); @@ -716,7 +836,7 @@ bool parseJsonStringLiteral(const std::string& json, size_t& pos, std::string& o case 'r': output += '\r'; break; case 't': output += '\t'; break; case 'u': - // 当前接口字段只需要保留UTF-8原文;遇到unicode转义时不执行错误拆分。 + // 当前接口字段只需要保留 UTF-8 原文;遇到 unicode 转义时不执行错误拆分。 output += "\\u"; for (int i = 0; i < 4 && pos < json.size(); ++i) { output += json[pos++]; @@ -731,8 +851,11 @@ bool parseJsonStringLiteral(const std::string& json, size_t& pos, std::string& o return false; } -// 解析签到接口使用的扁平JSON对象,支持字符串转义、逗号和冒号。 +// 解析签到接口使用的扁平 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; @@ -780,7 +903,15 @@ std::map parseJSON(const std::string& json) { } // ============= 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; @@ -791,7 +922,9 @@ sqlite3* openSqliteDb() { 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; @@ -801,16 +934,23 @@ bool sqliteExec(sqlite3* db, const std::string& sql) { 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); @@ -819,7 +959,10 @@ bool copyFileBinary(const std::string& src, const std::string& dst) { return out.good(); } +// 函数作用:服务启动时备份 SQLite 数据库文件。 bool backupSqliteDatabaseOnStartup() { + // 服务启动前先备份现有数据库。 + // 如果后续版本升级或误操作导致数据异常,可以从 backups 目录恢复。 std::ifstream check(SQLITE_DB_FILE.c_str(), std::ios::binary); if (!check.is_open()) { return true; @@ -842,7 +985,10 @@ bool backupSqliteDatabaseOnStartup() { 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; @@ -853,7 +999,9 @@ bool sqliteSetKv(sqlite3* db, const std::string& key, const std::string& value) 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=?"; @@ -867,6 +1015,7 @@ std::string sqliteGetKv(sqlite3* db, const std::string& key, const std::string& return value; } +// 函数作用:打开数据库并写入一个键值配置。 bool sqliteSetKvValue(const std::string& key, const std::string& value) { sqlite3* db = openSqliteDb(); if (!db) return false; @@ -875,6 +1024,7 @@ bool sqliteSetKvValue(const std::string& key, const std::string& value) { return ok; } +// 函数作用:打开数据库并读取一个键值配置。 std::string sqliteGetKvValue(const std::string& key, const std::string& fallback) { sqlite3* db = openSqliteDb(); if (!db) return fallback; @@ -883,7 +1033,9 @@ std::string sqliteGetKvValue(const std::string& key, const std::string& fallback 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; @@ -895,15 +1047,22 @@ int sqliteCountRows(sqlite3* db, const std::string& table) { 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; @@ -923,7 +1082,10 @@ bool upsertClass(sqlite3* db, const std::string& class_id, const std::string& cl 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; @@ -939,6 +1101,8 @@ bool ensureClassExists(sqlite3* db, const std::string& class_id, const std::stri 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)) { @@ -969,6 +1133,9 @@ bool upsertStudent(sqlite3* db, const std::string& student_id, const std::string 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 = @@ -991,7 +1158,10 @@ bool upsertFaceProfile(sqlite3* db, const std::string& face_id, const std::strin 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; @@ -1011,13 +1181,17 @@ bool clearFaceProfileBinding(sqlite3* db, const std::string& face_id, const std: 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; @@ -1039,7 +1213,9 @@ bool facePhotoExists(sqlite3* db, const std::string& face_id) { 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; @@ -1056,6 +1232,8 @@ bool faceProfileExists(sqlite3* db, const std::string& face_id) { 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; @@ -1079,7 +1257,10 @@ std::vector unboundPhotoCandidates(sqlite3* db, 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; @@ -1093,7 +1274,11 @@ bool deleteFaceProfileOnly(sqlite3* db, const std::string& face_id) { return ok; } +// 函数作用:根据本地照片记录修复或补齐人脸档案。 int reconcileFaceProfilesWithPhotos(sqlite3* db, const std::string& now) { + // 对齐人脸档案与照片索引。 + // 典型问题:管理端提交了以 student_id 作为 face_id 的档案,但 K210 上传照片用的是原始 face_id。 + // 若能找到唯一候选照片,则自动修正映射,减少人工清理数据的成本。 if (!db) return 0; struct Row { @@ -1133,7 +1318,9 @@ int reconcileFaceProfilesWithPhotos(sqlite3* db, const std::string& now) { return fixed; } +// 函数作用:执行 SQLite quick_check 并返回检查结果。 bool sqliteQuickCheck(std::string& message) { + // SQLite 自带快速一致性检查,供 health/storage 接口判断数据库是否健康。 sqlite3* db = openSqliteDb(); if (!db) { message = "open_failed"; @@ -1152,7 +1339,9 @@ bool sqliteQuickCheck(std::string& message) { return ok; } +// 函数作用:统计本地人脸照片文件数量。 int countFacePhotoFiles() { + // 返回 face_photos 表中的照片数量,用于系统状态展示。 sqlite3* db = openSqliteDb(); if (!db) return 0; int count = sqliteCountRows(db, "face_photos"); @@ -1163,6 +1352,8 @@ int countFacePhotoFiles() { 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) " @@ -1186,6 +1377,7 @@ bool sqliteInsertRecord(sqlite3* db, const std::string& timestamp, const std::st 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); @@ -1193,12 +1385,17 @@ bool saveAttendanceRecord(const std::string& timestamp, const std::string& stude 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," @@ -1210,15 +1407,18 @@ bool initializeSqliteStorage() { ");" "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," @@ -1228,6 +1428,7 @@ bool initializeSqliteStorage() { "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," @@ -1237,6 +1438,7 @@ bool initializeSqliteStorage() { "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," @@ -1244,6 +1446,7 @@ bool initializeSqliteStorage() { "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," @@ -1253,6 +1456,7 @@ bool initializeSqliteStorage() { "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," @@ -1266,6 +1470,7 @@ bool initializeSqliteStorage() { } if (sqliteGetKv(db, "time_slots", "").empty()) { + // 首次启动写入默认配置,避免管理端读取空值时报错。 sqliteSetKv(db, "time_slots", DEFAULT_TIMESLOTS_JSON); } if (sqliteGetKv(db, "sync_request", "").empty()) { @@ -1285,7 +1490,10 @@ bool initializeSqliteStorage() { } // ============= 数据库操作 ============= +// 函数作用:分页读取考勤记录并生成 JSON 数组。 std::string readDatabase(int limit = -1, int offset = 0) { + // 读取考勤记录列表,默认按最新记录在前返回。 + // limit/offset 由管理端分页使用,避免一次性返回过多历史记录。 sqlite3* db = openSqliteDb(); if (!db) return "[]"; @@ -1327,7 +1535,9 @@ std::string readDatabase(int limit = -1, int offset = 0) { return json.str(); } +// 函数作用:查询考勤记录总数、今日记录数和服务器时间。 std::string getStatistics() { + // 返回总签到数和今日签到数,供管理端首页统计卡片使用。 sqlite3* db = openSqliteDb(); if (!db) return "{\"total\":0,\"today\":0}"; int total = 0; @@ -1358,7 +1568,10 @@ std::string getStatistics() { return json.str(); } +// 函数作用:清空考勤记录表。 bool clearDatabase() { + // 清空考勤记录,不清空学生、人脸和时间段配置。 + // 管理端用于重置测试数据时调用。 sqlite3* db = openSqliteDb(); if (!db) return false; bool ok = sqliteExec(db, "DELETE FROM records"); @@ -1367,7 +1580,10 @@ bool clearDatabase() { } // ============= 时间段配置管理 ============= +// 函数作用:保存管理端上传的签到时间段配置。 bool saveTimeSlots(const std::string& json_data) { + // 保存签到时间段配置。 + // STM32 会通过同步接口获取该配置,用于判断当前是否处于允许签到的时间段。 std::cout << "[TIMESLOTS] Attempting to save " << json_data.size() << " bytes" << std::endl; if (json_data.empty()) { @@ -1384,13 +1600,18 @@ bool saveTimeSlots(const std::string& json_data) { 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(); @@ -1417,7 +1638,9 @@ std::string readStudents() { return json.str(); } +// 函数作用:读取班级配置列表并返回 JSON。 std::string readClasses() { + // 返回班级列表,包括默认班级 "0"。 std::ostringstream json; json << "{\"classes\":["; sqlite3* db = openSqliteDb(); @@ -1441,7 +1664,13 @@ std::string readClasses() { 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(); @@ -1486,7 +1715,10 @@ std::string readFaceProfiles() { 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; @@ -1511,6 +1743,8 @@ bool saveStudents(const std::string& json_data) { } if (ok) { + // 将本次上传列表中不存在的旧学生标记为 deleted。 + // 同时解除这些学生的人脸绑定,防止 K210 继续把已删除学生作为有效身份上报。 sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, "UPDATE students SET status='deleted',updated_at=? WHERE student_id=?", @@ -1570,7 +1804,10 @@ bool saveStudents(const std::string& json_data) { 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; @@ -1642,7 +1879,14 @@ bool saveClasses(const std::string& json_data) { 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; @@ -1663,6 +1907,8 @@ bool saveFaceProfiles(const std::string& json_data) { 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(); @@ -1684,6 +1930,7 @@ bool saveFaceProfiles(const std::string& json_data) { } if (!student_id.empty()) { + // 重新绑定人脸时,取消该 face_id 之前可能存在的待删除任务。 cancelPendingDeleteForFace(db, effective_face_id, now); } @@ -1711,6 +1958,8 @@ bool saveFaceProfiles(const std::string& json_data) { } 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=?", @@ -1746,7 +1995,10 @@ bool saveFaceProfiles(const std::string& json_data) { 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); @@ -1757,14 +2009,20 @@ unsigned long long readSyncVersion() { 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; @@ -1775,7 +2033,10 @@ unsigned long long parseVersionParam(const std::map& p 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 == '{') ? '}' : ']'; @@ -1807,7 +2068,10 @@ size_t findMatchingJsonEnd(const std::string& text, size_t start) { 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; @@ -1829,7 +2093,9 @@ std::string extractJsonMemberValue(const std::string& json, const std::string& k 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; @@ -1839,7 +2105,9 @@ std::string extractJsonStringValue(const std::string& json, const std::string& k 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; @@ -1855,7 +2123,10 @@ std::vector extractJsonStringArray(const std::string& json, const s 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; @@ -1902,7 +2173,9 @@ std::vector> parseJsonObjectArray(const std:: 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; @@ -1918,7 +2191,13 @@ unsigned long long jsonNumberValue(const std::string& json, const std::string& k 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); @@ -1935,6 +2214,9 @@ std::string makeSyncStateResponse(const std::string& client, unsigned long long 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", "[]"); @@ -1947,6 +2229,8 @@ std::string makeSyncStateResponse(const std::string& client, unsigned long long } 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; @@ -1954,6 +2238,7 @@ std::string makeSyncStateResponse(const std::string& client, unsigned long long } if (client == "qt" || client == "all" || client.empty()) { + // Qt 管理端需要完整状态,用于刷新页面和导出统计。 if (!first) json << ","; json << "\"students\":" << extractJsonMemberValue(readStudents(), "students", "[]") << ",\"classes\":" << extractJsonMemberValue(readClasses(), "classes", "[]"); @@ -1972,7 +2257,10 @@ std::string makeSyncStateResponse(const std::string& client, unsigned long long 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"); @@ -1980,12 +2268,14 @@ std::string makeSyncAckResponse(const std::string& request_body) { 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++; } @@ -2005,6 +2295,7 @@ std::string makeSyncAckResponse(const std::string& request_body) { return json.str(); } +// 函数作用:生成当前同步版本号响应。 std::string makeSyncVersionResponse() { std::ostringstream json; json << "{" @@ -2014,7 +2305,9 @@ std::string makeSyncVersionResponse() { return json.str(); } +// 函数作用:生成数据库和照片存储状态响应。 std::string makeStorageStatusResponse() { + // 返回存储层状态,包括 quick_check、表行数、同步版本和各设备同步进度。 std::string quick_check_message; const bool quick_ok = sqliteQuickCheck(quick_check_message); sqlite3* db = openSqliteDb(); @@ -2047,7 +2340,9 @@ std::string makeStorageStatusResponse() { return json.str(); } +// 函数作用:生成系统健康检查响应。 std::string makeHealthResponse() { + // 健康检查接口,供管理端或脚本确认服务端是否可用。 std::string quick_check_message; const bool quick_ok = sqliteQuickCheck(quick_check_message); std::ostringstream json; @@ -2066,7 +2361,10 @@ std::string makeHealthResponse() { // ============= 检查重复签到 ============= // 参数: 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); @@ -2103,7 +2401,9 @@ std::string checkDuplicate(const std::string& student_id, const std::string& slo } // ============= 删除记录 ============= +// 函数作用:按时间戳和学号删除单条考勤记录。 bool deleteRecord(const std::string& timestamp, const std::string& student_id) { + // 按时间戳和学号删除单条考勤记录。 sqlite3* db = openSqliteDb(); if (!db) return false; sqlite3_stmt* stmt = nullptr; @@ -2122,7 +2422,10 @@ bool deleteRecord(const std::string& timestamp, const std::string& student_id) { // ============= 人脸照片管理函数 ============= // 获取人脸照片列表 +// 函数作用:读取人脸照片列表并生成 JSON 响应。 std::string getFacePhotosList() { + // 返回服务端已保存的人脸照片列表。 + // K210 启动时会读取该接口,判断本地照片哪些还需要补传。 std::ostringstream json; json << "{\"photos\":["; @@ -2153,7 +2456,10 @@ std::string getFacePhotosList() { } // 保存人脸照片 +// 函数作用:保存指定人脸编号对应的照片文件和数据库记录。 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)) { @@ -2170,6 +2476,8 @@ bool saveFacePhoto(const std::string& face_id, const std::string& photo_data) { 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, @@ -2184,6 +2492,7 @@ bool saveFacePhoto(const std::string& face_id, const std::string& photo_data) { } sqlite3_finalize(stmt); + // student_id 为空表示此时只有人脸照片,还没有绑定学生。 upsertFaceProfile(db, face_id, "", "active", now); reconcileFaceProfilesWithPhotos(db, now); sqlite3_close(db); @@ -2193,7 +2502,9 @@ bool saveFacePhoto(const std::string& face_id, const std::string& photo_data) { } // 读取人脸照片 +// 函数作用:读取指定人脸编号对应的照片二进制数据。 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); @@ -2204,7 +2515,10 @@ std::string readFacePhoto(const std::string& face_id) { } // 删除人脸照片 +// 函数作用:删除指定人脸编号对应的照片文件和记录。 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(); @@ -2227,7 +2541,9 @@ bool deleteFacePhoto(const std::string& face_id) { } // 删除指定学生ID的所有考勤记录 +// 函数作用:删除指定学生关联的全部考勤记录并返回删除数量。 int deleteRecordsByStudentId(const std::string& student_id) { + // 删除某学生全部历史考勤记录,供管理端删除人脸时选择性联动清理。 sqlite3* db = openSqliteDb(); if (!db) return 0; sqlite3_stmt* stmt = nullptr; @@ -2243,7 +2559,10 @@ int deleteRecordsByStudentId(const std::string& student_id) { 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; @@ -2259,7 +2578,12 @@ std::vector findFaceIdsForStudent(sqlite3* db, const std::string& s 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; @@ -2284,7 +2608,9 @@ bool updateDeviceSyncState(const std::string& client, unsigned long long seen_ve return ok; } +// 函数作用:读取各客户端设备的同步状态摘要。 std::string getDeviceSyncSummary() { + // 返回所有客户端同步状态,给 /api/v1/system/storage 展示。 sqlite3* db = openSqliteDb(); if (!db) return "[]"; sqlite3_stmt* stmt = nullptr; @@ -2311,7 +2637,10 @@ std::string getDeviceSyncSummary() { } // 添加人脸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; @@ -2334,7 +2663,11 @@ bool addToPendingDelete(const std::string& face_id, const std::string& student_i } // 获取待删除列表 +// 函数作用:读取待删除人脸任务列表并生成 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; @@ -2382,7 +2715,10 @@ std::string getPendingDeleteList() { } // 从待删除列表中移除指定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; @@ -2401,7 +2737,10 @@ bool removeFromPendingDelete(const std::string& face_id_or_task_id) { return ok; } +// 函数作用:取消指定人脸编号对应的待删除任务。 bool cancelPendingDeleteForFace(sqlite3* db, const std::string& face_id, const std::string& now) { + // 某 face_id 重新上传照片或重新绑定时,说明它不应该再被删除。 + // 将未确认删除任务标记为 acked,防止终端误删。 if (!db || !isSafeId(face_id)) return false; sqlite3_stmt* stmt = nullptr; bool ok = false; @@ -2422,7 +2761,10 @@ bool cancelPendingDeleteForFace(sqlite3* db, const std::string& face_id, const s // ============= K210同步请求管理 ============= // 设置同步请求标志(QT端调用) +// 函数作用:设置是否请求 K210 立即执行同步。 bool setSyncRequest(bool sync) { + // sync_request 是给 K210 轮询的轻量标志。 + // 管理端保存人脸档案或删除人脸后置为 true,K210 看到后再拉取统一同步状态。 if (!sqliteSetKvValue("sync_request", std::string("{\"sync\":") + (sync ? "true" : "false") + "}")) { return false; } @@ -2431,17 +2773,24 @@ bool setSyncRequest(bool sync) { } // 获取同步请求状态(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); @@ -2470,11 +2819,14 @@ void handleHttpRequest(SOCKET client_socket) { 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; @@ -2498,6 +2850,8 @@ void handleHttpRequest(SOCKET client_socket) { } 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)) { @@ -2521,6 +2875,7 @@ void handleHttpRequest(SOCKET 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"; @@ -2529,6 +2884,7 @@ void handleHttpRequest(SOCKET client_socket) { } if (!isAuthorizedRequest(request, method, endpoint)) { + // 所有业务接口都需要 API Token,防止局域网中未授权设备调用写接口。 response_body = makeJsonError("Unauthorized"); status_code = 401; status_text = "Unauthorized"; @@ -2536,7 +2892,8 @@ void handleHttpRequest(SOCKET client_socket) { return; } - // 提取请求体 + // 提取请求体。 + // 首次 recv 可能只读到部分 body,因此需要根据 Content-Length 继续补读。 std::string request_body; if (method == "POST") { size_t content_length = 0; @@ -2555,6 +2912,7 @@ void handleHttpRequest(SOCKET client_socket) { 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); @@ -2601,6 +2959,8 @@ void handleHttpRequest(SOCKET client_socket) { } if (!is_photo_upload && !encrypted_body) { + // 除照片上传外,POST JSON 必须加密。 + // 这样学生档案、人脸绑定、同步 ACK 等管理数据不会明文传输。 response_body = makeJsonError("Encrypted request body required"); status_code = 400; status_text = "Bad Request"; @@ -2609,6 +2969,7 @@ void handleHttpRequest(SOCKET client_socket) { } 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"); @@ -2632,15 +2993,19 @@ void handleHttpRequest(SOCKET client_socket) { } // ============= 路由处理 ============= + // 路由按 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); @@ -2650,10 +3015,13 @@ void handleHttpRequest(SOCKET client_socket) { << ", 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); @@ -2672,6 +3040,7 @@ void handleHttpRequest(SOCKET client_socket) { 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"; @@ -2682,6 +3051,7 @@ void handleHttpRequest(SOCKET client_socket) { 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"); @@ -2698,6 +3068,7 @@ void handleHttpRequest(SOCKET client_socket) { } } else if (method == "GET" && endpoint == "/api/v1/attendance/records") { + // 管理端分页获取考勤记录。 auto params = parseQueryParams(query); int limit = 100; int offset = 0; @@ -2715,9 +3086,11 @@ void handleHttpRequest(SOCKET client_socket) { } } 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"); @@ -2728,6 +3101,7 @@ void handleHttpRequest(SOCKET client_socket) { } } 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"]; @@ -2748,9 +3122,11 @@ void handleHttpRequest(SOCKET client_socket) { } } 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") { @@ -2805,6 +3181,7 @@ void handleHttpRequest(SOCKET client_socket) { 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); @@ -2876,6 +3253,8 @@ void handleHttpRequest(SOCKET client_socket) { 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"); @@ -2910,6 +3289,7 @@ void handleHttpRequest(SOCKET client_socket) { } 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; @@ -2934,6 +3314,9 @@ void handleHttpRequest(SOCKET client_socket) { } 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"); @@ -2964,6 +3347,7 @@ void handleHttpRequest(SOCKET client_socket) { int records_deleted = 0; if (delete_records) { + // 可选联动删除该学生历史考勤记录。 records_deleted = deleteRecordsByStudentId(requested_id); } @@ -3015,7 +3399,10 @@ void handleHttpRequest(SOCKET client_socket) { } // ============= 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; @@ -3023,14 +3410,17 @@ void httpServerThread() { } 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); @@ -3049,6 +3439,8 @@ void httpServerThread() { 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); @@ -3059,8 +3451,10 @@ void httpServerThread() { } } +// 函数作用:服务端程序入口,初始化存储并启动 HTTP 服务。 int main() { #ifdef _WIN32 + // Windows 下使用 Winsock 前必须初始化 WSA。 WSADATA wsa; WSAStartup(MAKEWORD(2, 2), &wsa); #endif @@ -3074,18 +3468,19 @@ int main() { std::cout << "----------------------------------------" << std::endl; if (!backupSqliteDatabaseOnStartup()) { + // 备份失败时直接停止,避免在不确定状态下继续写数据库。 std::cerr << "[BACKUP] Startup backup failed, stop to avoid unsafe writes" << std::endl; return 1; } - // 初始化SQLite,所有运行时数据统一写入 attendance.db + // 初始化 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服务器 + // 启动 HTTP 服务器。主线程 join,服务进程会一直运行直到被外部终止。 std::thread http_thread(httpServerThread); http_thread.join();