Files
bishe-Server/server_encrypted.cpp
T
2026-05-04 21:17:56 +08:00

3098 lines
116 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 增强版考勤服务器 - 支持AES加密数据
*
* 功能:
* 1. HTTP端口8082 - 接收STM32加密签到数据
* 2. HTTP API - 提供Qt客户端接口
* 3. AES-128-CBC解密支持
*
* 编译:g++ -o server_encrypted.exe server_encrypted.cpp -lws2_32 -lcrypto -lssl -lsqlite3 -std=c++11
*/
#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <thread>
#include <vector>
#include <map>
#include <set>
#include <ctime>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <cstdio>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <direct.h>
#pragma comment(lib, "ws2_32.lib")
typedef int socklen_t;
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <cstring>
#include <sys/stat.h>
#define SOCKET int
#define INVALID_SOCKET -1
#define SOCKET_ERROR -1
#define closesocket(s) close(s)
#define _mkdir(d) mkdir(d, 0755)
#endif
// OpenSSL库用于AES解密和Base64
#include <openssl/evp.h>
#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/aes.h>
#include <sqlite3.h>
const int HTTP_PORT = 8082;
const int BUFFER_SIZE = 4096;
const std::string SQLITE_DB_FILE = "attendance.db";
const std::string LOG_FILE = "attendance.log";
const std::string FACES_DIR = "faces"; // 人脸照片目录
const std::string DB_BACKUP_DIR = "backups";
const size_t MAX_JSON_BODY_SIZE = 64 * 1024;
const size_t MAX_PHOTO_BODY_SIZE = 512 * 1024;
const 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;
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);
}
const std::string& apiToken() {
static const std::string token = getEnvOrDefault("ATTENDANCE_API_TOKEN", DEFAULT_API_TOKEN);
return token;
}
const std::string& allowedCorsOrigin() {
static const std::string origin = getEnvOrDefault("ATTENDANCE_CORS_ORIGIN", DEFAULT_CORS_ORIGIN);
return origin;
}
const std::string& bindAddress() {
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);
std::string jsonEscape(const std::string& value) {
std::ostringstream out;
for (unsigned char c : value) {
switch (c) {
case '\"': out << "\\\""; break;
case '\\': out << "\\\\"; break;
case '\b': out << "\\b"; break;
case '\f': out << "\\f"; break;
case '\n': out << "\\n"; break;
case '\r': out << "\\r"; break;
case '\t': out << "\\t"; break;
default:
if (c < 0x20) {
const char* hex = "0123456789abcdef";
out << "\\u00" << hex[(c >> 4) & 0x0f] << hex[c & 0x0f];
} else {
out << static_cast<char>(c);
}
break;
}
}
return out.str();
}
std::string csvEscape(const std::string& value) {
bool needs_quotes = false;
for (char c : value) {
if (c == ',' || c == '"' || c == '\r' || c == '\n') {
needs_quotes = true;
break;
}
}
if (!needs_quotes) {
return value;
}
std::string escaped = "\"";
for (char c : value) {
if (c == '"') {
escaped += "\"\"";
} else {
escaped += c;
}
}
escaped += "\"";
return escaped;
}
bool parseIntInRange(const std::string& value, int min_value, int max_value, int& output) {
if (value.empty()) {
return false;
}
try {
size_t pos = 0;
int parsed = std::stoi(value, &pos);
if (pos != value.size() || parsed < min_value || parsed > max_value) {
return false;
}
output = parsed;
return true;
} catch (...) {
return false;
}
}
bool parseTimeParts(const std::string& value, int& hour, int& minute) {
if (value.size() != 5 || value[2] != ':'
|| !std::isdigit(static_cast<unsigned char>(value[0]))
|| !std::isdigit(static_cast<unsigned char>(value[1]))
|| !std::isdigit(static_cast<unsigned char>(value[3]))
|| !std::isdigit(static_cast<unsigned char>(value[4]))) {
return false;
}
int parsed_hour = (value[0] - '0') * 10 + (value[1] - '0');
int parsed_minute = (value[3] - '0') * 10 + (value[4] - '0');
if (parsed_hour < 0 || parsed_hour > 23 || parsed_minute < 0 || parsed_minute > 59) {
return false;
}
hour = parsed_hour;
minute = parsed_minute;
return true;
}
bool isSafeId(const std::string& value) {
if (value.empty() || value.size() > 64) {
return false;
}
for (unsigned char c : value) {
if (!(std::isalnum(c) || c == '_' || c == '-')) {
return false;
}
}
return true;
}
bool buildFacePhotoPath(const std::string& face_id, std::string& filepath) {
if (!isSafeId(face_id)) {
return false;
}
filepath = FACES_DIR + "/" + face_id + ".jpg";
return true;
}
bool fileExistsAndNotEmpty(const std::string& filepath) {
std::ifstream file(filepath.c_str(), std::ios::binary | std::ios::ate);
return file.is_open() && file.tellg() > 0;
}
bool getHeaderValue(const std::string& request, const std::string& header_name, std::string& output) {
size_t headers_end = request.find("\r\n\r\n");
std::string headers = headers_end == std::string::npos ? request : request.substr(0, headers_end);
std::istringstream stream(headers);
std::string line;
std::string expected_key = header_name;
std::transform(expected_key.begin(), expected_key.end(), expected_key.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
while (std::getline(stream, line)) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
size_t colon = line.find(':');
if (colon == std::string::npos) {
continue;
}
std::string key = line.substr(0, colon);
std::string value = line.substr(colon + 1);
key.erase(std::remove_if(key.begin(), key.end(),
[](unsigned char c) { return std::isspace(c); }), key.end());
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (key != expected_key) {
continue;
}
size_t first = value.find_first_not_of(" \t");
if (first == std::string::npos) {
output.clear();
} else {
value.erase(0, first);
size_t last = value.find_last_not_of(" \t");
if (last != std::string::npos) {
value.erase(last + 1);
}
output = value;
}
return true;
}
return false;
}
bool hasHeaderToken(const std::string& request, const std::string& header_name, const std::string& expected) {
size_t headers_end = request.find("\r\n\r\n");
std::string headers = headers_end == std::string::npos ? request : request.substr(0, headers_end);
std::istringstream stream(headers);
std::string line;
while (std::getline(stream, line)) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
size_t colon = line.find(':');
if (colon == std::string::npos) {
continue;
}
std::string key = line.substr(0, colon);
std::string value = line.substr(colon + 1);
key.erase(std::remove_if(key.begin(), key.end(),
[](unsigned char c) { return std::isspace(c); }), key.end());
size_t first = value.find_first_not_of(" \t");
if (first == std::string::npos) {
value.clear();
} else {
value.erase(0, first);
size_t last = value.find_last_not_of(" \t");
if (last != std::string::npos) {
value.erase(last + 1);
}
}
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
std::string expected_key = header_name;
std::transform(expected_key.begin(), expected_key.end(), expected_key.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (key == expected_key && value == expected) {
return true;
}
}
return false;
}
bool hasBearerToken(const std::string& request, const std::string& expected) {
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;
return method == "OPTIONS";
}
bool isAuthorizedRequest(const std::string& request, const std::string& method, const std::string& endpoint) {
if (isPublicEndpoint(method, endpoint)) {
return true;
}
return hasHeaderToken(request, "X-Api-Token", apiToken()) || hasBearerToken(request, apiToken());
}
std::string makeJsonError(const std::string& message) {
return "{\"success\":false,\"message\":\"" + jsonEscape(message) + "\"}";
}
bool isEncryptedRequest(const std::string& request) {
return hasHeaderToken(request, "X-Encrypted", "AES128-CBC");
}
bool isJsonLikeContentType(const std::string& content_type) {
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必须与STM32端完全一致!
static const unsigned char MY_AES_KEY[16] = {
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c
};
static const unsigned char MY_AES_IV[16] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
};
// ============= Base64解码 =============
std::string base64_decode(const std::string& encoded_string) {
BIO *bio, *b64;
int decode_len = 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填充 =============
std::string add_pkcs7_padding(const std::string& data) {
size_t padding_len = 16 - (data.length() % 16);
std::string result = data;
for (size_t i = 0; i < padding_len; i++) {
result += (char)padding_len;
}
return result;
}
// ============= 手动移除PKCS#7填充 =============
std::string remove_pkcs7_padding(const std::string& data) {
if (data.empty()) return "";
// 获取最后一个字节(填充长度)
unsigned char padding_len = (unsigned char)data[data.length() - 1];
// 验证填充长度
if (padding_len < 1 || padding_len > 16 || padding_len > data.length()) {
std::cerr << "[CRYPTO] Invalid padding length: " << (int)padding_len << std::endl;
return data; // 返回原数据
}
// 验证所有填充字节都相同
for (size_t i = data.length() - padding_len; i < data.length(); i++) {
if ((unsigned char)data[i] != padding_len) {
std::cerr << "[CRYPTO] Invalid padding at position " << i << std::endl;
return data; // 返回原数据
}
}
// 移除填充
return data.substr(0, data.length() - padding_len);
}
// ============= AES-CBC加密 (用于响应Qt客户端) =============
std::string aes_encrypt_cbc(const std::string& plaintext) {
// 添加PKCS#7填充
std::string padded = add_pkcs7_padding(plaintext);
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) {
std::cerr << "[CRYPTO] Failed to create context" << std::endl;
return "";
}
// 初始化加密
if (EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, MY_AES_KEY, MY_AES_IV) != 1) {
std::cerr << "[CRYPTO] EncryptInit failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
// 禁用自动填充(我们已手动添加)
EVP_CIPHER_CTX_set_padding(ctx, 0);
// 加密数据
std::vector<unsigned char> ciphertext(padded.length() + 16);
int len;
int ciphertext_len = 0;
if (EVP_EncryptUpdate(ctx, ciphertext.data(), &len,
(unsigned char*)padded.c_str(),
padded.length()) != 1) {
std::cerr << "[CRYPTO] EncryptUpdate failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
ciphertext_len = len;
// 完成加密
if (EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len) != 1) {
std::cerr << "[CRYPTO] EncryptFinal failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
ciphertext_len += len;
EVP_CIPHER_CTX_free(ctx);
return std::string((char*)ciphertext.data(), ciphertext_len);
}
// ============= AES-CBC解密 (使用OpenSSL标准库) =============
std::string aes_decrypt_cbc(const std::string& encrypted_data) {
if (encrypted_data.empty() || encrypted_data.length() % 16 != 0) {
std::cerr << "[CRYPTO] Invalid encrypted data length: " << encrypted_data.length() << std::endl;
return "";
}
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) {
std::cerr << "[CRYPTO] Failed to create context" << std::endl;
return "";
}
// 初始化解密(使用标准AES-128-CBC)
if (EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, MY_AES_KEY, MY_AES_IV) != 1) {
std::cerr << "[CRYPTO] DecryptInit failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
// 禁用自动填充,我们手动处理PKCS#7
EVP_CIPHER_CTX_set_padding(ctx, 0);
// 解密数据
std::vector<unsigned char> plaintext(encrypted_data.length() + 16);
int len;
int plaintext_len = 0;
if (EVP_DecryptUpdate(ctx, plaintext.data(), &len,
(unsigned char*)encrypted_data.c_str(),
encrypted_data.length()) != 1) {
std::cerr << "[CRYPTO] DecryptUpdate failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
plaintext_len = len;
// 完成解密
if (EVP_DecryptFinal_ex(ctx, plaintext.data() + len, &len) != 1) {
std::cerr << "[CRYPTO] DecryptFinal failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
plaintext_len += len;
EVP_CIPHER_CTX_free(ctx);
std::string result((char*)plaintext.data(), plaintext_len);
// 手动移除PKCS#7填充
result = remove_pkcs7_padding(result);
return result;
}
// ============= 十六进制输出辅助函数 =============
void print_hex(const std::string& label, const unsigned char* data, size_t len) {
std::cout << label << " (" << len << " bytes): ";
for (size_t i = 0; i < len && i < 32; i++) {
printf("%02x ", data[i]);
}
if (len > 32) std::cout << "...";
std::cout << std::endl;
}
// ============= Base64编码 =============
std::string base64_encode(const std::string& input) {
BIO *bio, *b64;
BUF_MEM *bufferPtr;
b64 = BIO_new(BIO_f_base64());
bio = BIO_new(BIO_s_mem());
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
BIO_write(bio, input.c_str(), input.length());
BIO_flush(bio);
BIO_get_mem_ptr(bio, &bufferPtr);
std::string result(bufferPtr->data, bufferPtr->length);
BIO_free_all(bio);
return result;
}
// ============= 加密传输数据 =============
std::string encryptPayload(const std::string& plain_data) {
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;
}
std::string encryptJSON(const std::string& json_data) {
return encryptPayload(json_data);
}
// ============= 解密传输数据 =============
std::string decryptPayload(const std::string& encrypted_base64) {
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 "";
}
}
std::string decryptJSON(const std::string& encrypted_base64) {
return decryptPayload(encrypted_base64);
}
// ============= 工具函数 =============
std::string getCurrentTimestamp() {
time_t now = time(0);
tm ltm;
#ifdef _WIN32
localtime_s(&ltm, &now);
#else
localtime_r(&now, &ltm);
#endif
char buffer[80];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &ltm);
return std::string(buffer);
}
// 清理字符串中的控制字符(\r, \n等)
std::string cleanString(const std::string& str) {
std::string result;
result.reserve(str.length());
for (char c : str) {
// 只保留可打印字符和空格,过滤掉\r\n\t等控制字符
if (c >= 32 && c <= 126) {
result += c;
}
}
return result;
}
std::string urlDecode(const std::string& str) {
std::string result;
for (size_t i = 0; i < str.length(); ++i) {
if (str[i] == '%' && i + 2 < str.length()) {
int value;
std::istringstream is(str.substr(i + 1, 2));
if (is >> std::hex >> value) {
result += static_cast<char>(value);
i += 2;
} else {
result += str[i];
}
} else if (str[i] == '+') {
result += ' ';
} else {
result += str[i];
}
}
return result;
}
std::map<std::string, std::string> parseQueryParams(const std::string& query) {
std::map<std::string, std::string> params;
std::istringstream stream(query);
std::string pair;
while (std::getline(stream, pair, '&')) {
size_t pos = pair.find('=');
if (pos != std::string::npos) {
std::string key = urlDecode(pair.substr(0, pos));
std::string value = urlDecode(pair.substr(pos + 1));
params[key] = value;
}
}
return params;
}
bool parseJsonStringLiteral(const std::string& json, size_t& pos, std::string& output) {
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对象,支持字符串转义、逗号和冒号。
std::map<std::string, std::string> parseJSON(const std::string& json) {
std::map<std::string, std::string> result;
size_t pos = json.find('{');
if (pos == std::string::npos) return result;
pos++;
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存储层 =============
sqlite3* openSqliteDb() {
sqlite3* db = nullptr;
if (sqlite3_open(SQLITE_DB_FILE.c_str(), &db) != SQLITE_OK) {
std::cerr << "[SQLITE] Open failed: " << (db ? sqlite3_errmsg(db) : "unknown") << std::endl;
if (db) sqlite3_close(db);
return nullptr;
}
sqlite3_busy_timeout(db, 3000);
return db;
}
bool sqliteExec(sqlite3* db, const std::string& sql) {
char* err = nullptr;
if (sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &err) != SQLITE_OK) {
std::cerr << "[SQLITE] Exec failed: " << (err ? err : sqlite3_errmsg(db)) << std::endl;
sqlite3_free(err);
return false;
}
return true;
}
std::string sqliteColumnText(sqlite3_stmt* stmt, int col) {
const unsigned char* text = sqlite3_column_text(stmt, col);
return text ? reinterpret_cast<const char*>(text) : "";
}
bool sqliteBindText(sqlite3_stmt* stmt, int index, const std::string& value) {
return sqlite3_bind_text(stmt, index, value.c_str(), -1, SQLITE_TRANSIENT) == SQLITE_OK;
}
bool copyFileBinary(const std::string& src, const std::string& dst) {
std::ifstream in(src.c_str(), std::ios::binary);
if (!in.is_open()) return false;
std::ofstream out(dst.c_str(), std::ios::binary);
if (!out.is_open()) return false;
out << in.rdbuf();
return out.good();
}
bool backupSqliteDatabaseOnStartup() {
std::ifstream check(SQLITE_DB_FILE.c_str(), std::ios::binary);
if (!check.is_open()) {
return true;
}
check.close();
_mkdir(DB_BACKUP_DIR.c_str());
std::string ts = getCurrentTimestamp();
std::replace(ts.begin(), ts.end(), ':', '-');
std::replace(ts.begin(), ts.end(), ' ', '_');
const std::string backup_path = DB_BACKUP_DIR + "/attendance_" + ts + ".db";
if (!copyFileBinary(SQLITE_DB_FILE, backup_path)) {
std::cerr << "[BACKUP] Failed to create SQLite backup: " << backup_path << std::endl;
return false;
}
std::cout << "[BACKUP] SQLite backup created: " << backup_path << std::endl;
return true;
}
bool sqliteSetKv(sqlite3* db, const std::string& key, const std::string& value) {
sqlite3_stmt* stmt = nullptr;
const char* sql = "REPLACE INTO kv_store(key,value) VALUES(?,?)";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false;
sqliteBindText(stmt, 1, key);
sqliteBindText(stmt, 2, value);
bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return ok;
}
std::string sqliteGetKv(sqlite3* db, const std::string& key, const std::string& fallback) {
sqlite3_stmt* stmt = nullptr;
std::string value = fallback;
const char* sql = "SELECT value FROM kv_store WHERE key=?";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, key);
if (sqlite3_step(stmt) == SQLITE_ROW) {
value = sqliteColumnText(stmt, 0);
}
}
sqlite3_finalize(stmt);
return value;
}
bool sqliteSetKvValue(const std::string& key, const std::string& value) {
sqlite3* db = openSqliteDb();
if (!db) return false;
bool ok = sqliteSetKv(db, key, value);
sqlite3_close(db);
return ok;
}
std::string sqliteGetKvValue(const std::string& key, const std::string& fallback) {
sqlite3* db = openSqliteDb();
if (!db) return fallback;
std::string value = sqliteGetKv(db, key, fallback);
sqlite3_close(db);
return value;
}
int sqliteCountRows(sqlite3* db, const std::string& table) {
sqlite3_stmt* stmt = nullptr;
std::string sql = "SELECT COUNT(*) FROM " + table;
int count = 0;
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK
&& sqlite3_step(stmt) == SQLITE_ROW) {
count = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
return count;
}
std::string normalizedClassId(const std::string& class_id) {
return class_id.empty() ? "0" : class_id;
}
std::string defaultClassName(const std::string& 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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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;
}
bool sqliteQuickCheck(std::string& message) {
sqlite3* db = openSqliteDb();
if (!db) {
message = "open_failed";
return false;
}
sqlite3_stmt* stmt = nullptr;
bool ok = false;
message = "unknown";
if (sqlite3_prepare_v2(db, "PRAGMA quick_check", -1, &stmt, nullptr) == SQLITE_OK
&& sqlite3_step(stmt) == SQLITE_ROW) {
message = sqliteColumnText(stmt, 0);
ok = message == "ok";
}
sqlite3_finalize(stmt);
sqlite3_close(db);
return ok;
}
int countFacePhotoFiles() {
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) {
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;
}
bool initializeSqliteStorage() {
sqlite3* db = openSqliteDb();
if (!db) return false;
bool ok = sqliteExec(db,
"PRAGMA journal_mode=WAL;"
"CREATE TABLE IF NOT EXISTS records("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"timestamp TEXT NOT NULL,"
"student_id TEXT NOT NULL,"
"name TEXT,"
"confidence TEXT,"
"status TEXT,"
"class_id TEXT DEFAULT '0'"
");"
"CREATE INDEX IF NOT EXISTS idx_records_student_time ON records(student_id,timestamp);"
"CREATE INDEX IF NOT EXISTS idx_records_timestamp ON records(timestamp);"
"CREATE TABLE IF NOT EXISTS kv_store("
"key TEXT PRIMARY KEY,"
"value TEXT NOT NULL"
");"
"CREATE TABLE IF NOT EXISTS classes("
"class_id TEXT PRIMARY KEY,"
"class_name TEXT NOT NULL,"
"updated_at TEXT NOT NULL"
");"
"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);"
"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);"
"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"
");"
"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);"
"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;
}
// ============= 数据库操作 =============
std::string readDatabase(int limit = -1, int offset = 0) {
sqlite3* db = openSqliteDb();
if (!db) return "[]";
std::ostringstream json;
json << "[";
sqlite3_stmt* stmt = nullptr;
std::string sql = "SELECT timestamp,student_id,name,confidence,status,class_id FROM records ORDER BY id DESC";
if (limit > 0) {
sql += " LIMIT ? OFFSET ?";
}
bool first = true;
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) {
if (limit > 0) {
sqlite3_bind_int(stmt, 1, limit);
sqlite3_bind_int(stmt, 2, offset);
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
if (!first) json << ",";
first = false;
std::string conf = sqliteColumnText(stmt, 3);
if (conf.empty()) conf = "0";
std::string class_id = sqliteColumnText(stmt, 5);
if (class_id.empty()) class_id = "0";
json << "{"
<< "\"timestamp\":\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\","
<< "\"student_id\":\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\","
<< "\"name\":\"" << jsonEscape(sqliteColumnText(stmt, 2)) << "\","
<< "\"confidence\":" << conf << ","
<< "\"status\":\"" << jsonEscape(sqliteColumnText(stmt, 4)) << "\","
<< "\"class_id\":\"" << jsonEscape(class_id) << "\""
<< "}";
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
json << "]";
return json.str();
}
std::string getStatistics() {
sqlite3* db = openSqliteDb();
if (!db) return "{\"total\":0,\"today\":0}";
int total = 0;
int today = 0;
std::string todayDate = getCurrentTimestamp().substr(0, 10);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM records", -1, &stmt, nullptr) == SQLITE_OK
&& sqlite3_step(stmt) == SQLITE_ROW) {
total = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
stmt = nullptr;
if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM records WHERE substr(timestamp,1,10)=?", -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, todayDate);
if (sqlite3_step(stmt) == SQLITE_ROW) {
today = sqlite3_column_int(stmt, 0);
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
std::ostringstream json;
json << "{"
<< "\"total\":" << total << ","
<< "\"today\":" << today << ","
<< "\"server_time\":\"" << getCurrentTimestamp() << "\""
<< "}";
return json.str();
}
bool clearDatabase() {
sqlite3* db = openSqliteDb();
if (!db) return false;
bool ok = sqliteExec(db, "DELETE FROM records");
sqlite3_close(db);
return ok;
}
// ============= 时间段配置管理 =============
bool saveTimeSlots(const std::string& json_data) {
std::cout << "[TIMESLOTS] Attempting to save " << json_data.size() << " bytes" << std::endl;
if (json_data.empty()) {
std::cerr << "[TIMESLOTS] ERROR: json_data is empty!" << std::endl;
return false;
}
if (!sqliteSetKvValue("time_slots", json_data)) {
std::cerr << "[TIMESLOTS] Failed to save configuration to SQLite" << std::endl;
return false;
}
std::cout << "[TIMESLOTS] ✓ Configuration saved successfully to SQLite" << std::endl;
std::cout << "[TIMESLOTS] Preview: " << json_data.substr(0, 100) << "..." << std::endl;
return true;
}
std::string readTimeSlots() {
std::string content = sqliteGetKvValue("time_slots", DEFAULT_TIMESLOTS_JSON);
std::cout << "[TIMESLOTS] Read " << content.size() << " bytes from SQLite" << std::endl;
return content;
}
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();
}
std::string readClasses() {
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();
}
std::string readFaceProfiles() {
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) {
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) {
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) {
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)) {
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()) {
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) {
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() {
unsigned long long version = 1;
std::string value = sqliteGetKvValue("sync_version", "1");
std::istringstream ss(value);
if (!(ss >> version)) {
version = 1;
}
if (version == 0) version = 1;
return version;
}
unsigned long long bumpSyncVersion(const std::string& reason) {
unsigned long long version = readSyncVersion() + 1;
sqliteSetKvValue("sync_version", std::to_string(version));
std::cout << "[SYNC] Version bumped to " << version << " (" << reason << ")" << std::endl;
return version;
}
unsigned long long parseVersionParam(const std::map<std::string, std::string>& params) {
auto it = params.find("last_version");
if (it == params.end()) return 0;
unsigned long long value = 0;
for (char c : it->second) {
if (!std::isdigit(static_cast<unsigned char>(c))) return 0;
value = value * 10 + static_cast<unsigned long long>(c - '0');
}
return value;
}
size_t findMatchingJsonEnd(const std::string& text, size_t start) {
if (start >= text.size()) return std::string::npos;
const char open = text[start];
const char close = (open == '{') ? '}' : ']';
int depth = 0;
bool in_string = false;
bool escape = false;
for (size_t i = start; i < text.size(); ++i) {
char c = text[i];
if (escape) {
escape = false;
continue;
}
if (c == '\\' && in_string) {
escape = true;
continue;
}
if (c == '"') {
in_string = !in_string;
continue;
}
if (in_string) continue;
if (c == open) depth++;
if (c == close) {
depth--;
if (depth == 0) return i;
}
}
return std::string::npos;
}
std::string extractJsonMemberValue(const std::string& json, const std::string& key, const std::string& fallback) {
std::string marker = "\"" + key + "\":";
size_t pos = json.find(marker);
if (pos == std::string::npos) return fallback;
pos += marker.size();
while (pos < json.size() && std::isspace(static_cast<unsigned char>(json[pos]))) pos++;
if (pos >= json.size()) return fallback;
if (json[pos] == '{' || json[pos] == '[') {
size_t end = findMatchingJsonEnd(json, pos);
if (end != std::string::npos) {
return json.substr(pos, end - pos + 1);
}
}
size_t end = json.find_first_of(",}", pos);
if (end != std::string::npos) {
return json.substr(pos, end - pos);
}
return fallback;
}
std::string extractJsonStringValue(const std::string& json, const std::string& key, const std::string& fallback = "") {
std::string marker = "\"" + key + "\":\"";
size_t pos = json.find(marker);
if (pos == std::string::npos) return fallback;
pos += marker.size();
size_t end = json.find("\"", pos);
if (end == std::string::npos) return fallback;
return json.substr(pos, end - pos);
}
std::vector<std::string> extractJsonStringArray(const std::string& json, const std::string& key) {
std::vector<std::string> values;
std::string arr = extractJsonMemberValue(json, key, "[]");
if (arr.size() < 2) return values;
size_t pos = 0;
while (pos < arr.size()) {
size_t start = arr.find("\"", pos);
if (start == std::string::npos) break;
size_t end = arr.find("\"", start + 1);
if (end == std::string::npos) break;
values.push_back(arr.substr(start + 1, end - start - 1));
pos = end + 1;
}
return values;
}
std::vector<std::map<std::string, std::string>> parseJsonObjectArray(const std::string& json_data, const std::string& key) {
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;
}
unsigned long long jsonNumberValue(const std::string& json, const std::string& key, unsigned long long fallback) {
std::string marker = "\"" + key + "\":";
size_t pos = json.find(marker);
if (pos == std::string::npos) return fallback;
pos += marker.size();
while (pos < json.size() && std::isspace(static_cast<unsigned char>(json[pos]))) pos++;
unsigned long long value = 0;
bool has_digit = false;
while (pos < json.size() && std::isdigit(static_cast<unsigned char>(json[pos]))) {
has_digit = true;
value = value * 10ULL + static_cast<unsigned long long>(json[pos] - '0');
pos++;
}
return has_digit ? value : fallback;
}
std::string makeSyncStateResponse(const std::string& client, unsigned long long last_version) {
const unsigned long long version = readSyncVersion();
const bool changed = last_version < version;
updateDeviceSyncState(client.empty() ? "all" : client, version, 0);
std::ostringstream json;
json << "{\"version\":" << version
<< ",\"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()) {
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()) {
if (!first) json << ",";
const std::string slots = extractJsonMemberValue(readTimeSlots(), "time_slots", "[]");
json << "\"time_slots\":" << slots;
first = false;
}
if (client == "qt" || client == "all" || client.empty()) {
if (!first) json << ",";
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) {
const std::string client = extractJsonStringValue(request_body, "client", "unknown");
const unsigned long long ack_version = static_cast<unsigned long long>(jsonNumberValue(request_body, "version", 0));
const std::vector<std::string> deleted = extractJsonStringArray(request_body, "deleted");
const std::vector<std::string> task_ids = extractJsonStringArray(request_body, "task_ids");
int confirmed_delete_count = 0;
for (const std::string& task_id : task_ids) {
if (!task_id.empty() && removeFromPendingDelete(task_id)) {
confirmed_delete_count++;
}
}
for (const std::string& id : deleted) {
if (!id.empty() && removeFromPendingDelete(id)) {
confirmed_delete_count++;
}
}
updateDeviceSyncState(client, readSyncVersion(), ack_version);
if (confirmed_delete_count > 0) {
bumpSyncVersion("sync_ack_delete");
}
std::ostringstream json;
json << "{\"success\":true,"
<< "\"client\":\"" << jsonEscape(client) << "\","
<< "\"version\":" << readSyncVersion() << ","
<< "\"confirmed_delete_count\":" << confirmed_delete_count << "}";
return json.str();
}
std::string makeSyncVersionResponse() {
std::ostringstream json;
json << "{"
<< "\"version\":" << readSyncVersion() << ","
<< "\"server_time\":\"" << jsonEscape(getCurrentTimestamp()) << "\""
<< "}";
return json.str();
}
std::string makeStorageStatusResponse() {
std::string quick_check_message;
const bool quick_ok = sqliteQuickCheck(quick_check_message);
sqlite3* db = openSqliteDb();
int records = 0;
int face_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) {
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;
}
// ============= 人脸照片管理函数 =============
// 获取人脸照片列表
std::string getFacePhotosList() {
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) {
_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();
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);
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) {
std::string filepath;
if (!buildFacePhotoPath(face_id, filepath)) return "";
std::ifstream file(filepath, std::ios::binary);
if (!file.is_open()) return "";
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
return content;
}
// 删除人脸照片
bool deleteFacePhoto(const std::string& face_id) {
std::string filepath;
if (!buildFacePhotoPath(face_id, filepath)) return false;
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) {
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) {
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() {
sqlite3* db = openSqliteDb();
if (!db) return "[]";
sqlite3_stmt* stmt = nullptr;
std::ostringstream json;
json << "[";
bool first = true;
const char* sql = "SELECT client,last_seen_version,last_ack_version,last_seen_at FROM device_sync_state ORDER BY client";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
if (!first) json << ",";
json << "{"
<< "\"client\":\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\","
<< "\"last_seen_version\":" << sqlite3_column_int64(stmt, 1) << ","
<< "\"last_ack_version\":" << sqlite3_column_int64(stmt, 2) << ","
<< "\"last_seen_at\":\"" << jsonEscape(sqliteColumnText(stmt, 3)) << "\""
<< "}";
first = false;
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
json << "]";
return json.str();
}
// 添加人脸ID到待删除列表
bool addToPendingDelete(const std::string& face_id, const std::string& student_id) {
sqlite3* db = openSqliteDb();
if (!db) return false;
sqlite3_stmt* stmt = nullptr;
bool ok = false;
std::string task_id = face_id + "-" + std::to_string(static_cast<long long>(time(0)));
if (sqlite3_prepare_v2(db,
"REPLACE INTO pending_delete(task_id,face_id,student_id,created_at,status) VALUES(?,?,?,?,?)",
-1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, task_id);
sqliteBindText(stmt, 2, face_id);
sqliteBindText(stmt, 3, student_id);
sqliteBindText(stmt, 4, getCurrentTimestamp());
sqliteBindText(stmt, 5, "pending");
ok = sqlite3_step(stmt) == SQLITE_DONE;
}
sqlite3_finalize(stmt);
sqlite3_close(db);
std::cout << "[PENDING_DELETE] Added: " << face_id << " task=" << task_id << std::endl;
return ok;
}
// 获取待删除列表
std::string getPendingDeleteList() {
sqlite3* db = openSqliteDb();
if (!db) return "{\"pending_delete\":[],\"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) {
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) {
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端调用)
bool setSyncRequest(bool sync) {
if (!sqliteSetKvValue("sync_request", std::string("{\"sync\":") + (sync ? "true" : "false") + "}")) {
return false;
}
std::cout << "[SYNC] Sync request set to: " << (sync ? "true" : "false") << std::endl;
return true;
}
// 获取同步请求状态(K210轮询调用)
std::string getSyncRequest() {
return sqliteGetKvValue("sync_request", "{\"sync\":false}");
}
// 清除同步请求标志(K210确认后调用)
bool clearSyncRequest() {
return setSyncRequest(false);
}
// ============= HTTP请求处理 =============
void handleHttpRequest(SOCKET client_socket) {
char buffer[BUFFER_SIZE] = {0};
int bytes = recv(client_socket, buffer, BUFFER_SIZE - 1, 0);
if (bytes <= 0) {
closesocket(client_socket);
return;
}
std::string request(buffer, bytes);
std::istringstream stream(request);
std::string method, path, version;
stream >> method >> path >> version;
std::cout << "[HTTP] " << method << " " << path << std::endl;
std::string 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 = [&]() {
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) {
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;
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) {
response_body = "{\"error\":\"Not Found\"}";
status_code = 404;
status_text = "Not Found";
sendResponse();
return;
}
if (!isAuthorizedRequest(request, method, endpoint)) {
response_body = makeJsonError("Unauthorized");
status_code = 401;
status_text = "Unauthorized";
sendResponse();
return;
}
// 提取请求体
std::string request_body;
if (method == "POST") {
size_t content_length = 0;
std::string cl_str;
if (getHeaderValue(request, "Content-Length", cl_str)) {
int parsed_content_length = 0;
if (!parseIntInRange(cl_str, 0, static_cast<int>(MAX_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);
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) {
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;
}
}
// ============= 路由处理 =============
if (method == "OPTIONS") {
response_body = "{}";
} else if (method == "GET" && endpoint == "/api/v1/sync/version") {
response_body = makeSyncVersionResponse();
std::cout << "[SYNC] Version fetched: " << readSyncVersion() << std::endl;
} else if (method == "GET" && endpoint == "/api/v1/sync/state") {
auto params = parseQueryParams(query);
const std::string client = params.count("client") ? params["client"] : "all";
const unsigned long long last_version = parseVersionParam(params);
response_body = makeSyncStateResponse(client, last_version);
std::cout << "[SYNC] State fetched: client=" << client
<< ", last_version=" << last_version
<< ", current_version=" << readSyncVersion() << std::endl;
} else if (method == "POST" && endpoint == "/api/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") {
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(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") {
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>
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
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服务器线程 =============
void httpServerThread() {
SOCKET server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == INVALID_SOCKET) {
std::cerr << "[HTTP] Failed to create socket" << std::endl;
return;
}
int opt = 1;
setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
const std::string bind_host = bindAddress();
if (bind_host == "0.0.0.0" || bind_host.empty()) {
server_addr.sin_addr.s_addr = INADDR_ANY;
} else {
if (inet_pton(AF_INET, bind_host.c_str(), &server_addr.sin_addr) != 1) {
std::cerr << "[HTTP] Invalid ATTENDANCE_BIND_HOST: " << bind_host << std::endl;
closesocket(server_socket);
return;
}
}
server_addr.sin_port = htons(HTTP_PORT);
if (bind(server_socket, (sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
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) {
sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
SOCKET client_socket = accept(server_socket, (sockaddr*)&client_addr, &client_len);
if (client_socket != INVALID_SOCKET) {
std::thread(handleHttpRequest, client_socket).detach();
}
}
}
int main() {
#ifdef _WIN32
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
#endif
std::cout << "========================================" << std::endl;
std::cout << " Face Recognition Attendance Server" << std::endl;
std::cout << " with AES-128-CBC Encryption" << std::endl;
std::cout << "========================================" << std::endl;
std::cout << "HTTP Port " << HTTP_PORT << " - Encrypted API" << std::endl;
std::cout << "Database: " << SQLITE_DB_FILE << std::endl;
std::cout << "----------------------------------------" << std::endl;
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服务器
std::thread http_thread(httpServerThread);
http_thread.join();
#ifdef _WIN32
WSACleanup();
#endif
return 0;
}