Files
bishe-Server/server_encrypted.cpp
2026-05-21 08:06:55 +08:00

3493 lines
146 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 增强版考勤服务器 - 支持 AES 加密数据
*
* 本文件是智慧课堂考勤系统的服务端主程序,采用“单文件 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 <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <thread>
#include <vector>
#include <map>
#include <set>
#include <ctime>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <cstdio>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <direct.h>
#pragma comment(lib, "ws2_32.lib")
typedef int socklen_t;
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <cstring>
#include <sys/stat.h>
#define SOCKET int
#define INVALID_SOCKET -1
#define SOCKET_ERROR -1
#define closesocket(s) close(s)
#define _mkdir(d) mkdir(d, 0755)
#endif
// OpenSSL库用于AES解密和Base64
#include <openssl/evp.h>
#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/aes.h>
#include <sqlite3.h>
const int HTTP_PORT = 8082; // 统一 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/<face_id>.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<std::map<std::string, std::string>> 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<char>(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<unsigned char>(value[0]))
|| !std::isdigit(static_cast<unsigned char>(value[1]))
|| !std::isdigit(static_cast<unsigned char>(value[3]))
|| !std::isdigit(static_cast<unsigned char>(value[4]))) {
return false;
}
int parsed_hour = (value[0] - '0') * 10 + (value[1] - '0');
int parsed_minute = (value[3] - '0') * 10 + (value[4] - '0');
if (parsed_hour < 0 || parsed_hour > 23 || parsed_minute < 0 || parsed_minute > 59) {
return false;
}
hour = parsed_hour;
minute = parsed_minute;
return true;
}
// 函数作用:校验编号字符串是否只包含允许用于路径和主键的安全字符。
bool isSafeId(const std::string& value) {
// 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<char>(std::tolower(c)); });
while (std::getline(stream, line)) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
size_t colon = line.find(':');
if (colon == std::string::npos) {
continue;
}
std::string key = line.substr(0, colon);
std::string value = line.substr(colon + 1);
key.erase(std::remove_if(key.begin(), key.end(),
[](unsigned char c) { return std::isspace(c); }), key.end());
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (key != expected_key) {
continue;
}
size_t first = value.find_first_not_of(" \t");
if (first == std::string::npos) {
output.clear();
} else {
value.erase(0, first);
size_t last = value.find_last_not_of(" \t");
if (last != std::string::npos) {
value.erase(last + 1);
}
output = value;
}
return true;
}
return false;
}
// 函数作用:判断指定请求头中的 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<char>(std::tolower(c)); });
std::string expected_key = header_name;
std::transform(expected_key.begin(), expected_key.end(), expected_key.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (key == expected_key && value == expected) {
return true;
}
}
return false;
}
// 函数作用:判断 Authorization Bearer Token 是否有效。
bool hasBearerToken(const std::string& request, const std::string& expected) {
// 兼容 Authorization: Bearer <token> 形式。
// 管理端或调试工具使用标准 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<char>(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<char>(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<int>(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<unsigned char> ciphertext(padded.length() + 16);
int len;
int ciphertext_len = 0;
if (EVP_EncryptUpdate(ctx, ciphertext.data(), &len,
(unsigned char*)padded.c_str(),
padded.length()) != 1) {
std::cerr << "[CRYPTO] EncryptUpdate failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
ciphertext_len = len;
// 完成加密并释放上下文。
if (EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len) != 1) {
std::cerr << "[CRYPTO] EncryptFinal failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
ciphertext_len += len;
EVP_CIPHER_CTX_free(ctx);
return std::string((char*)ciphertext.data(), ciphertext_len);
}
// ============= AES-CBC解密 (使用OpenSSL标准库) =============
// 函数作用:使用 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<unsigned char> plaintext(encrypted_data.length() + 16);
int len;
int plaintext_len = 0;
if (EVP_DecryptUpdate(ctx, plaintext.data(), &len,
(unsigned char*)encrypted_data.c_str(),
encrypted_data.length()) != 1) {
std::cerr << "[CRYPTO] DecryptUpdate failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
plaintext_len = len;
// 完成解密。
if (EVP_DecryptFinal_ex(ctx, plaintext.data() + len, &len) != 1) {
std::cerr << "[CRYPTO] DecryptFinal failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
plaintext_len += len;
EVP_CIPHER_CTX_free(ctx);
std::string result((char*)plaintext.data(), plaintext_len);
// 手动移除 PKCS#7 填充。
result = remove_pkcs7_padding(result);
return result;
}
// ============= 十六进制输出辅助函数 =============
// 函数作用:按十六进制格式打印调试用二进制数据。
void print_hex(const std::string& label, const unsigned char* data, size_t len) {
// 调试用:只打印前 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(&ltm, &now);
#else
localtime_r(&now, &ltm);
#endif
char buffer[80];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &ltm);
return std::string(buffer);
}
// 清理字符串中的控制字符(\r, \n等)
// 函数作用:清理字符串首尾空白和控制字符。
std::string cleanString(const std::string& str) {
// 过滤串口或 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<char>(value);
i += 2;
} else {
result += str[i];
}
} else if (str[i] == '+') {
result += ' ';
} else {
result += str[i];
}
}
return result;
}
// 函数作用:解析 URL 查询字符串为键值参数表。
std::map<std::string, std::string> parseQueryParams(const std::string& query) {
// 将 a=1&b=2 形式的查询字符串解析为 map,供分页、时间段、删除选项等接口使用。
std::map<std::string, std::string> params;
std::istringstream stream(query);
std::string pair;
while (std::getline(stream, pair, '&')) {
size_t pos = pair.find('=');
if (pos != std::string::npos) {
std::string key = urlDecode(pair.substr(0, pos));
std::string value = urlDecode(pair.substr(pos + 1));
params[key] = value;
}
}
return params;
}
// 函数作用:从 JSON 文本当前位置解析一个字符串字面量。
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<std::string, std::string> parseJSON(const std::string& json) {
// 本项目接口的 JSON 结构比较固定,为降低依赖复杂度没有引入完整 JSON 库。
// 该函数用于解析 {"student_id":"...","confidence":0.95} 这类扁平对象。
std::map<std::string, std::string> 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<unsigned char>(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<unsigned char>(json[pos]))) pos++;
if (pos >= json.size() || json[pos] != ':') {
result.clear();
return result;
}
pos++;
while (pos < json.size() && std::isspace(static_cast<unsigned char>(json[pos]))) pos++;
std::string value;
if (pos < json.size() && json[pos] == '"') {
if (!parseJsonStringLiteral(json, pos, value)) {
result.clear();
return result;
}
} else {
size_t start = pos;
while (pos < json.size() && json[pos] != ',' && json[pos] != '}') pos++;
value = json.substr(start, pos - start);
size_t first = value.find_first_not_of(" \t\r\n");
size_t last = value.find_last_not_of(" \t\r\n");
value = first == std::string::npos ? "" : value.substr(first, last - first + 1);
}
result[key] = value;
while (pos < json.size() && std::isspace(static_cast<unsigned char>(json[pos]))) pos++;
if (pos < json.size() && json[pos] == ',') pos++;
}
return result;
}
// ============= 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<const char*>(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<sqlite3_int64>(file.tellg());
}
// 函数作用:判断指定人脸照片记录和文件是否存在。
bool facePhotoExists(sqlite3* db, const std::string& face_id) {
// 判断人脸照片是否真实存在。
// 先查数据库索引,再回退检查 faces/<face_id>.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<std::string> unboundPhotoCandidates(sqlite3* db,
const std::string& wrong_face_id,
const std::string& student_id) {
// 修复管理端误把 student_id 当 face_id 提交的情况。
// 如果存在唯一一张“未绑定或已绑定同一学生”的照片,就把映射转移到真实 face_id。
std::vector<std::string> 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<Row> 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<std::string> 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<std::map<std::string, std::string>> 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<std::string> 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<std::string> 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<std::map<std::string, std::string>> classes = parseJsonObjectArray(json_data, "classes");
sqlite3* db = openSqliteDb();
if (!db) return false;
const std::string now = getCurrentTimestamp();
bool ok = sqliteExec(db, "BEGIN");
std::set<std::string> 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<std::string> 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<std::map<std::string, std::string>> 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<std::string> incoming_face_ids;
std::set<std::string> 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<std::string> 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<std::string> 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<std::string, std::string>& 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<unsigned char>(c))) return 0;
value = value * 10 + static_cast<unsigned long long>(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<unsigned char>(json[pos]))) pos++;
if (pos >= json.size()) return fallback;
if (json[pos] == '{' || json[pos] == '[') {
size_t end = findMatchingJsonEnd(json, pos);
if (end != std::string::npos) {
return json.substr(pos, end - pos + 1);
}
}
size_t end = json.find_first_of(",}", pos);
if (end != std::string::npos) {
return json.substr(pos, end - pos);
}
return fallback;
}
// 函数作用:从 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<std::string> extractJsonStringArray(const std::string& json, const std::string& key) {
// 提取简单字符串数组,例如 sync ack 中的 deleted 和 task_ids。
std::vector<std::string> values;
std::string arr = extractJsonMemberValue(json, key, "[]");
if (arr.size() < 2) return values;
size_t pos = 0;
while (pos < arr.size()) {
size_t start = arr.find("\"", pos);
if (start == std::string::npos) break;
size_t end = arr.find("\"", start + 1);
if (end == std::string::npos) break;
values.push_back(arr.substr(start + 1, end - start - 1));
pos = end + 1;
}
return values;
}
// 函数作用:从 JSON 文本中解析对象数组字段。
std::vector<std::map<std::string, std::string>> parseJsonObjectArray(const std::string& json_data, const std::string& key) {
// 解析 {"students":[{...},{...}]} 这类对象数组。
// 学生、班级、人脸档案保存接口都复用该轻量解析器。
std::vector<std::map<std::string, std::string>> 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<std::string, std::string> row;
size_t item_pos = 1;
while (item_pos < object.size()) {
while (item_pos < object.size() && std::isspace(static_cast<unsigned char>(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<unsigned char>(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<unsigned char>(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<unsigned char>(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<unsigned char>(json[pos]))) pos++;
unsigned long long value = 0;
bool has_digit = false;
while (pos < json.size() && std::isdigit(static_cast<unsigned char>(json[pos]))) {
has_digit = true;
value = value * 10ULL + static_cast<unsigned long long>(json[pos] - '0');
pos++;
}
return has_digit ? value : fallback;
}
// 函数作用:生成统一同步状态响应,按版本判断是否返回完整数据。
std::string makeSyncStateResponse(const std::string& client, unsigned long long last_version) {
// 构造统一同步响应。
// 客户端传入自己的 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<unsigned long long>(jsonNumberValue(request_body, "version", 0));
const std::vector<std::string> deleted = extractJsonStringArray(request_body, "deleted");
const std::vector<std::string> task_ids = extractJsonStringArray(request_body, "task_ids");
int confirmed_delete_count = 0;
for (const std::string& task_id : task_ids) {
// 优先按 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<sqlite3_int64>(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<char>(file)), std::istreambuf_iterator<char>());
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<std::string> findFaceIdsForStudent(sqlite3* db, const std::string& student_id) {
// 通过 student_id 找到所有绑定的人脸编号。
// 删除接口既支持传 face_id,也支持传 student_id,因此需要这个反查能力。
std::vector<std::string> 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<sqlite3_int64>(seen_version));
sqlite3_bind_int64(stmt, 3, static_cast<sqlite3_int64>(ack_version));
sqliteBindText(stmt, 4, getCurrentTimestamp());
ok = sqlite3_step(stmt) == SQLITE_DONE;
}
sqlite3_finalize(stmt);
sqlite3_close(db);
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<long long>(time(0)));
if (sqlite3_prepare_v2(db,
"REPLACE INTO pending_delete(task_id,face_id,student_id,created_at,status) VALUES(?,?,?,?,?)",
-1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, task_id);
sqliteBindText(stmt, 2, face_id);
sqliteBindText(stmt, 3, student_id);
sqliteBindText(stmt, 4, getCurrentTimestamp());
sqliteBindText(stmt, 5, "pending");
ok = sqlite3_step(stmt) == SQLITE_DONE;
}
sqlite3_finalize(stmt);
sqlite3_close(db);
std::cout << "[PENDING_DELETE] Added: " << face_id << " task=" << task_id << std::endl;
return ok;
}
// 获取待删除列表
// 函数作用:读取待删除人脸任务列表并生成 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<std::map<std::string, std::string>> tasks;
if (sqlite3_prepare_v2(db,
"SELECT task_id,face_id,student_id,created_at,status,COALESCE(ack_at,'') FROM pending_delete WHERE status<>'acked' ORDER BY created_at,face_id",
-1, &stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
std::map<std::string, std::string> row;
row["task_id"] = sqliteColumnText(stmt, 0);
row["face_id"] = sqliteColumnText(stmt, 1);
row["student_id"] = sqliteColumnText(stmt, 2);
row["created_at"] = sqliteColumnText(stmt, 3);
row["status"] = sqliteColumnText(stmt, 4);
row["ack_at"] = sqliteColumnText(stmt, 5);
tasks.push_back(row);
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
std::ostringstream json;
json << "{\"pending_delete\":[";
bool first = true;
for (const auto& task : tasks) {
if (!first) json << ",";
json << "\"" << jsonEscape(task.at("face_id")) << "\"";
first = false;
}
json << "],\"delete_tasks\":[";
first = true;
for (const auto& task : tasks) {
if (!first) json << ",";
json << "{"
<< "\"task_id\":\"" << jsonEscape(task.at("task_id")) << "\","
<< "\"face_id\":\"" << jsonEscape(task.at("face_id")) << "\","
<< "\"student_id\":\"" << jsonEscape(task.at("student_id")) << "\","
<< "\"created_at\":\"" << jsonEscape(task.at("created_at")) << "\","
<< "\"status\":\"" << jsonEscape(task.at("status")) << "\""
<< "}";
first = false;
}
json << "]}";
return json.str();
}
// 从待删除列表中移除指定ID(K210确认删除后调用)
// 函数作用:从待删除队列中移除指定任务或人脸编号。
bool removeFromPendingDelete(const std::string& face_id_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<int>(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<size_t>(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<int>(std::min(remaining, (size_t)BUFFER_SIZE)), 0);
if (extra_bytes <= 0) break;
request_body.append(extra_buffer, extra_bytes);
remaining -= extra_bytes;
}
}
}
std::cout << "[HTTP] POST body size: " << request_body.size()
<< " bytes (expected: " << content_length << ")" << std::endl;
if (request_body.size() != content_length) {
response_body = makeJsonError("Incomplete request body");
status_code = 400;
status_text = "Bad Request";
sendResponse();
return;
}
if (!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<long long>(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/<id>
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/<face_id>
// 该接口主要由 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/<face_id_or_student_id>?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<std::string> 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;
}