Files
bishe-Server/server_encrypted.cpp
T
2026-05-04 02:51:01 +08:00

3067 lines
114 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 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 readFaceIdMapping();
std::string getPendingDeleteList();
std::string getFacePhotosList();
std::string getAllStudentIds();
bool setSyncRequest(bool sync);
bool removeFromPendingDelete(const std::string& face_id);
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);
std::string readMappingConfig();
bool saveMappingConfig(const std::string& json_data);
std::map<std::string, std::string> parseFaceIdMappings(const std::string& json_data);
std::map<std::string, std::string> parseSimpleStringMap(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 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) {
return method == "OPTIONS" || (method == "GET" && endpoint == "/api/v1/system/health");
}
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) + "\"}";
}
// ⚠️ 重要:密钥和IV必须与STM32端完全一致!
static const unsigned char MY_AES_KEY[16] = {
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c
};
static const unsigned char MY_AES_IV[16] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
};
// ============= Base64解码 =============
std::string base64_decode(const std::string& encoded_string) {
BIO *bio, *b64;
int decode_len = encoded_string.length();
char* buffer = new char[decode_len];
bio = BIO_new_mem_buf(encoded_string.c_str(), -1);
b64 = BIO_new(BIO_f_base64());
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
int length = BIO_read(bio, buffer, decode_len);
BIO_free_all(bio);
std::string result(buffer, length);
delete[] buffer;
return result;
}
// ============= 添加PKCS#7填充 =============
std::string add_pkcs7_padding(const std::string& data) {
size_t padding_len = 16 - (data.length() % 16);
std::string result = data;
for (size_t i = 0; i < padding_len; i++) {
result += (char)padding_len;
}
return result;
}
// ============= 手动移除PKCS#7填充 =============
std::string remove_pkcs7_padding(const std::string& data) {
if (data.empty()) return "";
// 获取最后一个字节(填充长度)
unsigned char padding_len = (unsigned char)data[data.length() - 1];
// 验证填充长度
if (padding_len < 1 || padding_len > 16 || padding_len > data.length()) {
std::cerr << "[CRYPTO] Invalid padding length: " << (int)padding_len << std::endl;
return data; // 返回原数据
}
// 验证所有填充字节都相同
for (size_t i = data.length() - padding_len; i < data.length(); i++) {
if ((unsigned char)data[i] != padding_len) {
std::cerr << "[CRYPTO] Invalid padding at position " << i << std::endl;
return data; // 返回原数据
}
}
// 移除填充
return data.substr(0, data.length() - padding_len);
}
// ============= AES-CBC加密 (用于响应Qt客户端) =============
std::string aes_encrypt_cbc(const std::string& plaintext) {
// 添加PKCS#7填充
std::string padded = add_pkcs7_padding(plaintext);
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) {
std::cerr << "[CRYPTO] Failed to create context" << std::endl;
return "";
}
// 初始化加密
if (EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, MY_AES_KEY, MY_AES_IV) != 1) {
std::cerr << "[CRYPTO] EncryptInit failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
// 禁用自动填充(我们已手动添加)
EVP_CIPHER_CTX_set_padding(ctx, 0);
// 加密数据
std::vector<unsigned char> ciphertext(padded.length() + 16);
int len;
int ciphertext_len = 0;
if (EVP_EncryptUpdate(ctx, ciphertext.data(), &len,
(unsigned char*)padded.c_str(),
padded.length()) != 1) {
std::cerr << "[CRYPTO] EncryptUpdate failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
ciphertext_len = len;
// 完成加密
if (EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len) != 1) {
std::cerr << "[CRYPTO] EncryptFinal failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
ciphertext_len += len;
EVP_CIPHER_CTX_free(ctx);
return std::string((char*)ciphertext.data(), ciphertext_len);
}
// ============= AES-CBC解密 (使用OpenSSL标准库) =============
std::string aes_decrypt_cbc(const std::string& encrypted_data) {
if (encrypted_data.empty() || encrypted_data.length() % 16 != 0) {
std::cerr << "[CRYPTO] Invalid encrypted data length: " << encrypted_data.length() << std::endl;
return "";
}
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) {
std::cerr << "[CRYPTO] Failed to create context" << std::endl;
return "";
}
// 初始化解密(使用标准AES-128-CBC)
if (EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, MY_AES_KEY, MY_AES_IV) != 1) {
std::cerr << "[CRYPTO] DecryptInit failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
// 禁用自动填充,我们手动处理PKCS#7
EVP_CIPHER_CTX_set_padding(ctx, 0);
// 解密数据
std::vector<unsigned char> plaintext(encrypted_data.length() + 16);
int len;
int plaintext_len = 0;
if (EVP_DecryptUpdate(ctx, plaintext.data(), &len,
(unsigned char*)encrypted_data.c_str(),
encrypted_data.length()) != 1) {
std::cerr << "[CRYPTO] DecryptUpdate failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
plaintext_len = len;
// 完成解密
if (EVP_DecryptFinal_ex(ctx, plaintext.data() + len, &len) != 1) {
std::cerr << "[CRYPTO] DecryptFinal failed" << std::endl;
EVP_CIPHER_CTX_free(ctx);
return "";
}
plaintext_len += len;
EVP_CIPHER_CTX_free(ctx);
std::string result((char*)plaintext.data(), plaintext_len);
// 手动移除PKCS#7填充
result = remove_pkcs7_padding(result);
return result;
}
// ============= 十六进制输出辅助函数 =============
void print_hex(const std::string& label, const unsigned char* data, size_t len) {
std::cout << label << " (" << len << " bytes): ";
for (size_t i = 0; i < len && i < 32; i++) {
printf("%02x ", data[i]);
}
if (len > 32) std::cout << "...";
std::cout << std::endl;
}
// ============= Base64编码 =============
std::string base64_encode(const std::string& input) {
BIO *bio, *b64;
BUF_MEM *bufferPtr;
b64 = BIO_new(BIO_f_base64());
bio = BIO_new(BIO_s_mem());
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
BIO_write(bio, input.c_str(), input.length());
BIO_flush(bio);
BIO_get_mem_ptr(bio, &bufferPtr);
std::string result(bufferPtr->data, bufferPtr->length);
BIO_free_all(bio);
return result;
}
// ============= 加密JSON数据(用于响应Qt客户端)=============
std::string encryptJSON(const std::string& json_data) {
std::cout << "[CRYPTO] Encrypting response..." << std::endl;
// AES加密
std::string encrypted = aes_encrypt_cbc(json_data);
if (encrypted.empty()) {
std::cerr << "[CRYPTO] Encryption failed!" << std::endl;
return "";
}
// Base64编码
std::string encrypted_base64 = base64_encode(encrypted);
std::cout << "[CRYPTO] ✓ Response encrypted" << std::endl;
return encrypted_base64;
}
// ============= 解密JSON数据 =============
std::string decryptJSON(const std::string& encrypted_base64) {
try {
std::cout << "[CRYPTO] Decrypting data..." << std::endl;
std::cout << "[CRYPTO] Base64 input: " << encrypted_base64 << std::endl;
std::cout << "[CRYPTO] Base64 length: " << encrypted_base64.length() << std::endl;
// 1. Base64解码
std::string encrypted_binary = base64_decode(encrypted_base64);
std::cout << "[CRYPTO] Binary length: " << encrypted_binary.length() << std::endl;
print_hex("[CRYPTO] Encrypted binary", (unsigned char*)encrypted_binary.c_str(), encrypted_binary.length());
// 调试:打印密钥和IV
print_hex("[CRYPTO] Using KEY", MY_AES_KEY, 16);
print_hex("[CRYPTO] Using IV", MY_AES_IV, 16);
// 2. AES解密
std::string decrypted = aes_decrypt_cbc(encrypted_binary);
if (decrypted.empty()) {
std::cerr << "[CRYPTO] Decryption failed!" << std::endl;
return "";
}
std::cout << "[CRYPTO] ✓ Decryption successful" << std::endl;
std::cout << "[CRYPTO] Decrypted length: " << decrypted.length() << std::endl;
print_hex("[CRYPTO] Decrypted hex", (unsigned char*)decrypted.c_str(), decrypted.length());
std::cout << "[CRYPTO] Decrypted text: " << decrypted << std::endl;
return decrypted;
} catch (const std::exception& e) {
std::cerr << "[CRYPTO] Exception: " << e.what() << std::endl;
return "";
}
}
// ============= 工具函数 =============
std::string getCurrentTimestamp() {
time_t now = time(0);
tm ltm;
#ifdef _WIN32
localtime_s(&ltm, &now);
#else
localtime_r(&now, &ltm);
#endif
char buffer[80];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &ltm);
return std::string(buffer);
}
// 清理字符串中的控制字符(\r, \n等)
std::string cleanString(const std::string& str) {
std::string result;
result.reserve(str.length());
for (char c : str) {
// 只保留可打印字符和空格,过滤掉\r\n\t等控制字符
if (c >= 32 && c <= 126) {
result += c;
}
}
return result;
}
std::string urlDecode(const std::string& str) {
std::string result;
for (size_t i = 0; i < str.length(); ++i) {
if (str[i] == '%' && i + 2 < str.length()) {
int value;
std::istringstream is(str.substr(i + 1, 2));
if (is >> std::hex >> value) {
result += static_cast<char>(value);
i += 2;
} else {
result += str[i];
}
} else if (str[i] == '+') {
result += ' ';
} else {
result += str[i];
}
}
return result;
}
std::map<std::string, std::string> parseQueryParams(const std::string& query) {
std::map<std::string, std::string> params;
std::istringstream stream(query);
std::string pair;
while (std::getline(stream, pair, '&')) {
size_t pos = pair.find('=');
if (pos != std::string::npos) {
std::string key = urlDecode(pair.substr(0, pos));
std::string value = urlDecode(pair.substr(pos + 1));
params[key] = value;
}
}
return params;
}
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 sqliteTableHasRows(sqlite3* db, const std::string& table) {
std::string sql = "SELECT 1 FROM " + table + " LIMIT 1";
sqlite3_stmt* stmt = nullptr;
bool has_rows = false;
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) {
has_rows = sqlite3_step(stmt) == SQLITE_ROW;
}
sqlite3_finalize(stmt);
return has_rows;
}
bool sqliteTableHasColumn(sqlite3* db, const std::string& table, const std::string& column) {
sqlite3_stmt* stmt = nullptr;
bool found = false;
std::string sql = "PRAGMA table_info(" + table + ")";
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
if (sqliteColumnText(stmt, 1) == column) {
found = true;
break;
}
}
}
sqlite3_finalize(stmt);
return found;
}
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 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 face_mappings("
"face_id TEXT PRIMARY KEY,"
"student_id 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 (!sqliteTableHasColumn(db, "pending_delete", "task_id")) {
sqliteExec(db, "ALTER TABLE pending_delete ADD COLUMN task_id TEXT");
}
if (!sqliteTableHasColumn(db, "pending_delete", "student_id")) {
sqliteExec(db, "ALTER TABLE pending_delete ADD COLUMN student_id TEXT");
}
if (!sqliteTableHasColumn(db, "pending_delete", "created_at")) {
sqliteExec(db, "ALTER TABLE pending_delete ADD COLUMN created_at TEXT");
}
if (!sqliteTableHasColumn(db, "pending_delete", "status")) {
sqliteExec(db, "ALTER TABLE pending_delete ADD COLUMN status TEXT DEFAULT 'pending'");
}
if (!sqliteTableHasColumn(db, "pending_delete", "ack_at")) {
sqliteExec(db, "ALTER TABLE pending_delete ADD COLUMN ack_at TEXT");
}
sqliteExec(db, "UPDATE pending_delete SET task_id='migrated-' || face_id WHERE task_id IS NULL OR task_id=''");
sqliteExec(db, "UPDATE pending_delete SET created_at='" + getCurrentTimestamp() + "' WHERE created_at IS NULL OR created_at=''");
sqliteExec(db, "UPDATE pending_delete SET status='pending' WHERE status IS NULL OR status=''");
if (sqliteGetKv(db, "time_slots", "").empty()) {
sqliteSetKv(db, "time_slots", DEFAULT_TIMESLOTS_JSON);
}
if (sqliteGetKv(db, "mapping_config", "").empty()) {
sqliteSetKv(db, "mapping_config", "{\"student_name_map\":{},\"student_class_map\":{},\"class_name_map\":{}}");
}
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);
sqlite3_stmt* migration_stmt = nullptr;
if (sqlite3_prepare_v2(db, "SELECT face_id,student_id FROM face_mappings", -1, &migration_stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(migration_stmt) == SQLITE_ROW) {
upsertFaceProfile(db, sqliteColumnText(migration_stmt, 0), sqliteColumnText(migration_stmt, 1), "active", now);
}
}
sqlite3_finalize(migration_stmt);
if (!sqliteTableHasRows(db, "students")) {
const std::string mapping_config = sqliteGetKv(db, "mapping_config", "{\"student_name_map\":{},\"student_class_map\":{},\"class_name_map\":{}}");
const std::map<std::string, std::string> student_names = parseSimpleStringMap(mapping_config, "student_name_map");
const std::map<std::string, std::string> student_classes = parseSimpleStringMap(mapping_config, "student_class_map");
const std::map<std::string, std::string> class_names = parseSimpleStringMap(mapping_config, "class_name_map");
for (const auto& item : class_names) {
upsertClass(db, item.first, item.second, now);
}
for (const auto& item : student_names) {
const auto class_it = student_classes.find(item.first);
upsertStudent(db, item.first, item.second,
class_it == student_classes.end() ? "" : class_it->second, now);
}
for (const auto& item : student_classes) {
if (!student_names.count(item.first)) {
upsertStudent(db, item.first, "", item.second, now);
}
}
if (sqlite3_prepare_v2(db, "SELECT DISTINCT student_id,name,COALESCE(NULLIF(class_id,''),'0') FROM records WHERE student_id<>''",
-1, &migration_stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(migration_stmt) == SQLITE_ROW) {
upsertStudent(db, sqliteColumnText(migration_stmt, 0), sqliteColumnText(migration_stmt, 1),
sqliteColumnText(migration_stmt, 2), now);
}
}
sqlite3_finalize(migration_stmt);
migration_stmt = nullptr;
if (sqlite3_prepare_v2(db, "SELECT DISTINCT student_id FROM face_mappings WHERE student_id<>''",
-1, &migration_stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(migration_stmt) == SQLITE_ROW) {
upsertStudent(db, sqliteColumnText(migration_stmt, 0), "", "", now);
}
}
sqlite3_finalize(migration_stmt);
}
_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::map<std::string, std::string> parseSimpleStringMap(const std::string& json_data, const std::string& key) {
std::map<std::string, std::string> values;
std::string object = extractJsonMemberValue(json_data, key, "{}");
if (object.size() < 2) return values;
size_t pos = 1;
while (pos < object.size()) {
while (pos < object.size() && (std::isspace(static_cast<unsigned char>(object[pos])) || object[pos] == ',')) pos++;
if (pos >= object.size() || object[pos] == '}') break;
std::string item_key;
if (!parseJsonStringLiteral(object, pos, item_key)) break;
while (pos < object.size() && std::isspace(static_cast<unsigned char>(object[pos]))) pos++;
if (pos >= object.size() || object[pos] != ':') break;
pos++;
while (pos < object.size() && std::isspace(static_cast<unsigned char>(object[pos]))) pos++;
std::string item_value;
if (pos < object.size() && object[pos] == '"') {
if (!parseJsonStringLiteral(object, pos, item_value)) break;
} else {
size_t start = pos;
while (pos < object.size() && object[pos] != ',' && object[pos] != '}') pos++;
item_value = object.substr(start, pos - 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);
}
if (!item_key.empty() && !item_value.empty()) {
values[item_key] = item_value;
}
}
return values;
}
bool saveMappingConfig(const std::string& json_data) {
if (json_data.empty()) return false;
if (extractJsonMemberValue(json_data, "student_name_map", "").empty()
|| extractJsonMemberValue(json_data, "student_class_map", "").empty()
|| extractJsonMemberValue(json_data, "class_name_map", "").empty()) {
return false;
}
const std::map<std::string, std::string> student_names = parseSimpleStringMap(json_data, "student_name_map");
const std::map<std::string, std::string> student_classes = parseSimpleStringMap(json_data, "student_class_map");
const std::map<std::string, std::string> class_names = parseSimpleStringMap(json_data, "class_name_map");
sqlite3* db = openSqliteDb();
if (!db) return false;
const std::string now = getCurrentTimestamp();
bool ok = sqliteExec(db, "BEGIN")
&& sqliteSetKv(db, "mapping_config", json_data);
if (ok) {
for (const auto& item : class_names) {
if (!upsertClass(db, item.first, item.second, now)) {
ok = false;
break;
}
}
}
if (ok) {
for (const auto& item : student_names) {
const auto class_it = student_classes.find(item.first);
if (!upsertStudent(db, item.first, item.second,
class_it == student_classes.end() ? "" : class_it->second, now)) {
ok = false;
break;
}
}
}
if (ok) {
for (const auto& item : student_classes) {
if (student_names.count(item.first)) continue;
if (!upsertStudent(db, item.first, "", item.second, now)) {
ok = false;
break;
}
}
}
if (ok) {
sqlite3_stmt* clear_stmt = nullptr;
if (sqlite3_prepare_v2(db,
"UPDATE students SET student_name='',updated_at=? WHERE COALESCE(student_name,'')<>'' AND student_id=?",
-1, &clear_stmt, nullptr) == SQLITE_OK) {
sqlite3_stmt* select_stmt = nullptr;
std::vector<std::string> existing_ids;
if (sqlite3_prepare_v2(db,
"SELECT student_id FROM students WHERE COALESCE(student_name,'')<>''",
-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& student_id : existing_ids) {
if (student_names.count(student_id)) continue;
sqlite3_reset(clear_stmt);
sqlite3_clear_bindings(clear_stmt);
sqliteBindText(clear_stmt, 1, now);
sqliteBindText(clear_stmt, 2, student_id);
if (sqlite3_step(clear_stmt) != SQLITE_DONE) {
ok = false;
break;
}
}
} else {
ok = false;
}
sqlite3_finalize(clear_stmt);
}
if (ok) {
sqlite3_stmt* clear_stmt = nullptr;
if (sqlite3_prepare_v2(db,
"UPDATE students SET class_id='0',updated_at=? WHERE COALESCE(class_id,'0')<>'0' AND student_id=?",
-1, &clear_stmt, nullptr) == SQLITE_OK) {
sqlite3_stmt* select_stmt = nullptr;
std::vector<std::string> existing_ids;
if (sqlite3_prepare_v2(db,
"SELECT student_id FROM students WHERE COALESCE(class_id,'0')<>'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);
for (const std::string& student_id : existing_ids) {
if (student_classes.count(student_id)) continue;
sqlite3_reset(clear_stmt);
sqlite3_clear_bindings(clear_stmt);
sqliteBindText(clear_stmt, 1, now);
sqliteBindText(clear_stmt, 2, student_id);
if (sqlite3_step(clear_stmt) != SQLITE_DONE) {
ok = false;
break;
}
}
} else {
ok = false;
}
sqlite3_finalize(clear_stmt);
}
if (ok) {
sqlite3_stmt* clear_stmt = nullptr;
if (sqlite3_prepare_v2(db,
"UPDATE classes SET class_name='',updated_at=? WHERE class_id<>'0' AND COALESCE(class_name,'')<>'' AND class_id=?",
-1, &clear_stmt, nullptr) == SQLITE_OK) {
sqlite3_stmt* select_stmt = nullptr;
std::vector<std::string> existing_ids;
if (sqlite3_prepare_v2(db,
"SELECT class_id FROM classes WHERE class_id<>'0' AND COALESCE(class_name,'')<>''",
-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& class_id : existing_ids) {
if (class_names.count(class_id)) continue;
sqlite3_reset(clear_stmt);
sqlite3_clear_bindings(clear_stmt);
sqliteBindText(clear_stmt, 1, now);
sqliteBindText(clear_stmt, 2, class_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;
}
std::string readMappingConfig() {
sqlite3* db = openSqliteDb();
if (!db) return sqliteGetKvValue("mapping_config", "{\"student_name_map\":{},\"student_class_map\":{},\"class_name_map\":{}}");
std::ostringstream json;
json << "{\"student_name_map\":{";
sqlite3_stmt* stmt = nullptr;
bool first = true;
if (sqlite3_prepare_v2(db,
"SELECT student_id,student_name FROM students WHERE COALESCE(student_name,'')<>'' ORDER BY student_id",
-1, &stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
if (!first) json << ",";
json << "\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\":"
<< "\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\"";
first = false;
}
}
sqlite3_finalize(stmt);
json << "},\"student_class_map\":{";
stmt = nullptr;
first = true;
if (sqlite3_prepare_v2(db,
"SELECT student_id,class_id FROM students WHERE COALESCE(class_id,'0')<>'0' ORDER BY student_id",
-1, &stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
if (!first) json << ",";
json << "\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\":"
<< "\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\"";
first = false;
}
}
sqlite3_finalize(stmt);
json << "},\"class_name_map\":{";
stmt = nullptr;
first = true;
if (sqlite3_prepare_v2(db,
"SELECT class_id,class_name FROM classes WHERE COALESCE(class_name,'')<>'' ORDER BY class_id",
-1, &stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
if (!first) json << ",";
json << "\"" << jsonEscape(sqliteColumnText(stmt, 0)) << "\":"
<< "\"" << jsonEscape(sqliteColumnText(stmt, 1)) << "\"";
first = false;
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
json << "}}";
return json.str();
}
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;
}
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;
if (client == "k210" || client == "qt" || client == "all" || client.empty()) {
const std::string mappings = extractJsonMemberValue(readFaceIdMapping(), "mappings", "{}");
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_mappings\":" << mappings
<< ",\"pending_delete\":" << pending
<< ",\"delete_tasks\":" << delete_tasks;
first = false;
}
if (client == "stm32" || client == "qt" || client == "all" || client.empty()) {
if (!first) json << ",";
const std::string slots = extractJsonMemberValue(readTimeSlots(), "time_slots", "[]");
json << "\"time_slots\":" << slots;
first = false;
}
if (client == "qt" || client == "all" || client.empty()) {
if (!first) json << ",";
std::string mapping_config = readMappingConfig();
json << "\"mapping_config\":" << mapping_config
<< ",\"face_photos\":" << extractJsonMemberValue(getFacePhotosList(), "photos", "[]")
<< ",\"all_student_ids\":" << extractJsonMemberValue(getAllStudentIds(), "student_ids", "[]")
<< ",\"records\":" << readDatabase(1000, 0)
<< ",\"statistics\":" << getStatistics();
}
json << "}";
}
json << "}";
return json.str();
}
std::string makeSyncAckResponse(const std::string& request_body) {
const std::string client = extractJsonStringValue(request_body, "client", "unknown");
const 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_mappings = 0;
int pending_deletes = 0;
int face_photos = countFacePhotoFiles();
if (db) {
records = sqliteCountRows(db, "records");
face_mappings = sqliteCountRows(db, "face_mappings");
pending_deletes = sqliteCountRows(db, "pending_delete");
sqlite3_close(db);
}
std::ostringstream json;
json << "{"
<< "\"storage\":\"sqlite\","
<< "\"database\":\"" << jsonEscape(SQLITE_DB_FILE) << "\","
<< "\"ok\":" << (quick_ok ? "true" : "false") << ","
<< "\"quick_check\":\"" << jsonEscape(quick_check_message) << "\","
<< "\"records\":" << records << ","
<< "\"face_mappings\":" << face_mappings << ","
<< "\"face_photos\":" << face_photos << ","
<< "\"pending_deletes\":" << pending_deletes << ","
<< "\"sync_version\":" << readSyncVersion() << ","
<< "\"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;
}
// ============= 人脸照片管理函数 =============
// 保存人脸ID映射配置
bool saveFaceIdMapping(const std::string& json_data) {
std::cout << "[FACE_MAPPING] Saving " << json_data.size() << " bytes" << std::endl;
if (extractJsonMemberValue(json_data, "mappings", "").empty()) {
return false;
}
std::map<std::string, std::string> mappings = parseFaceIdMappings(json_data);
sqlite3* db = openSqliteDb();
if (!db) return false;
const std::string now = getCurrentTimestamp();
std::set<std::string> previously_bound_faces;
sqlite3_stmt* previous_stmt = nullptr;
if (sqlite3_prepare_v2(db, "SELECT face_id FROM face_mappings", -1, &previous_stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(previous_stmt) == SQLITE_ROW) {
previously_bound_faces.insert(sqliteColumnText(previous_stmt, 0));
}
}
sqlite3_finalize(previous_stmt);
bool ok = sqliteExec(db, "BEGIN")
&& sqliteExec(db, "DELETE FROM face_mappings");
sqlite3_stmt* stmt = nullptr;
const char* sql = "REPLACE INTO face_mappings(face_id,student_id) VALUES(?,?)";
if (ok && sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
for (const auto& m : mappings) {
if (!isSafeId(m.first) || !isSafeId(m.second)) {
ok = false;
break;
}
sqlite3_reset(stmt);
sqlite3_clear_bindings(stmt);
sqliteBindText(stmt, 1, m.first);
sqliteBindText(stmt, 2, m.second);
if (sqlite3_step(stmt) != SQLITE_DONE) {
ok = false;
break;
}
}
} else {
ok = false;
}
sqlite3_finalize(stmt);
if (ok) {
for (const auto& m : mappings) {
previously_bound_faces.erase(m.first);
if (!upsertFaceProfile(db, m.first, m.second, "active", now)) {
ok = false;
break;
}
}
}
if (ok) {
for (const auto& m : mappings) {
if (!upsertStudent(db, m.second, "", "", now)) {
ok = false;
break;
}
}
}
if (ok) {
for (const std::string& face_id : previously_bound_faces) {
if (!clearFaceProfileBinding(db, face_id, now)) {
ok = false;
break;
}
}
}
sqliteExec(db, ok ? "COMMIT" : "ROLLBACK");
sqlite3_close(db);
return ok;
}
std::map<std::string, std::string> parseFaceIdMappings(const std::string& json_data) {
return parseSimpleStringMap(json_data, "mappings");
}
std::map<std::string, std::string> loadFaceIdMappings() {
std::map<std::string, std::string> mappings;
sqlite3* db = openSqliteDb();
if (!db) return mappings;
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, "SELECT face_id,student_id FROM face_mappings ORDER BY face_id", -1, &stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
mappings[sqliteColumnText(stmt, 0)] = sqliteColumnText(stmt, 1);
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
return mappings;
}
std::set<std::string> resolveK210FaceIdsForDelete(const std::string& requested_id) {
std::set<std::string> ids;
bool matched_mapping = false;
const auto mappings = loadFaceIdMappings();
for (const auto& m : mappings) {
if (m.first == requested_id || m.second == requested_id) {
ids.insert(m.first);
matched_mapping = true;
}
}
if (!matched_mapping) {
ids.insert(requested_id);
}
return ids;
}
std::set<std::string> resolveStudentIdsForDelete(const std::string& requested_id) {
std::set<std::string> ids;
ids.insert(requested_id);
const auto mappings = loadFaceIdMappings();
for (const auto& m : mappings) {
if (m.first == requested_id || m.second == requested_id) {
ids.insert(m.second);
}
}
return ids;
}
// 读取人脸ID映射配置
std::string readFaceIdMapping() {
std::map<std::string, std::string> mappings = loadFaceIdMappings();
std::ostringstream json;
json << "{\"mappings\":{";
bool first = true;
for (const auto& m : mappings) {
if (!first) json << ",";
json << "\"" << jsonEscape(m.first) << "\":\"" << jsonEscape(m.second) << "\"";
first = false;
}
json << "}}";
return json.str();
}
// 获取人脸照片列表
std::string getFacePhotosList() {
std::ostringstream json;
json << "{\"photos\":[";
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) {
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, getCurrentTimestamp());
sqlite3_step(stmt);
}
sqlite3_finalize(stmt);
upsertFaceProfile(db, face_id, "", "active", getCurrentTimestamp());
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映射中删除指定ID
bool removeFaceIdFromMapping(const std::string& face_id) {
sqlite3* db = openSqliteDb();
if (!db) return false;
sqlite3_stmt* stmt = nullptr;
bool ok = false;
if (sqlite3_prepare_v2(db, "DELETE FROM face_mappings WHERE face_id=?", -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, face_id);
ok = sqlite3_step(stmt) == SQLITE_DONE && sqlite3_changes(db) > 0;
}
sqlite3_finalize(stmt);
clearFaceProfileBinding(db, face_id, getCurrentTimestamp());
sqlite3_close(db);
if (ok) std::cout << "[FACE_MAPPING] Removed face_id: " << face_id << std::endl;
return ok;
}
// 删除指定学生ID的所有考勤记录
int deleteRecordsByStudentId(const std::string& student_id) {
sqlite3* db = openSqliteDb();
if (!db) return 0;
sqlite3_stmt* stmt = nullptr;
int deleted_count = 0;
if (sqlite3_prepare_v2(db, "DELETE FROM records WHERE student_id=?", -1, &stmt, nullptr) == SQLITE_OK) {
sqliteBindText(stmt, 1, student_id);
if (sqlite3_step(stmt) == SQLITE_DONE) {
deleted_count = sqlite3_changes(db);
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
return deleted_count;
}
// 获取所有学生ID(优先使用结构化学生表,同时包含考勤记录和人脸映射中的学号)
std::string getAllStudentIds() {
std::set<std::string> student_ids;
sqlite3* db = openSqliteDb();
if (db) {
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db,
"SELECT student_id FROM students WHERE student_id<>'' "
"UNION SELECT student_id FROM records WHERE student_id<>'' "
"UNION SELECT student_id FROM face_mappings WHERE student_id<>'' "
"ORDER BY student_id",
-1, &stmt, nullptr) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
student_ids.insert(sqliteColumnText(stmt, 0));
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
}
// 构建JSON响应
std::ostringstream json;
json << "{\"student_ids\":[";
bool first = true;
for (const auto& id : student_ids) {
if (!first) json << ",";
json << "\"" << jsonEscape(id) << "\"";
first = false;
}
json << "]}";
return json.str();
}
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;
}
// ============= K210同步请求管理 =============
// 设置同步请求标志(QT端调用)
bool setSyncRequest(bool sync) {
if (!sqliteSetKvValue("sync_request", std::string("{\"sync\":") + (sync ? "true" : "false") + "}")) {
return false;
}
std::cout << "[SYNC] Sync request set to: " << (sync ? "true" : "false") << std::endl;
return true;
}
// 获取同步请求状态(K210轮询调用)
std::string getSyncRequest() {
return sqliteGetKvValue("sync_request", "{\"sync\":false}");
}
// 清除同步请求标志(K210确认后调用)
bool clearSyncRequest() {
return setSyncRequest(false);
}
// 根据K210的face_id查询真实学号
std::string getMappedStudentId(const std::string& face_id) {
std::cout << "[MAPPING] Looking up face_id: " << face_id << std::endl;
const auto mappings = loadFaceIdMappings();
auto it = mappings.find(face_id);
if (it != mappings.end() && !it->second.empty()) {
std::cout << "[MAPPING] ✓ Face ID " << face_id << " -> Student ID " << it->second << std::endl;
return it->second;
}
std::cout << "[MAPPING] No mapping found for face_id: " << face_id << std::endl;
return face_id; // 没找到映射,返回原ID
}
// ============= HTTP请求处理 =============
void handleHttpRequest(SOCKET client_socket) {
char buffer[BUFFER_SIZE] = {0};
int bytes = recv(client_socket, buffer, BUFFER_SIZE - 1, 0);
if (bytes <= 0) {
closesocket(client_socket);
return;
}
std::string request(buffer, bytes);
std::istringstream stream(request);
std::string method, path, version;
stream >> method >> path >> version;
std::cout << "[HTTP] " << method << " " << path << std::endl;
std::string response_body;
std::string content_type = "application/json";
int status_code = 200;
std::string status_text = "OK";
auto sendResponse = [&]() {
std::ostringstream response;
response << "HTTP/1.1 " << status_code << " " << status_text << "\r\n";
response << "Content-Type: " << content_type << "; charset=utf-8\r\n";
response << "Content-Length: " << response_body.length() << "\r\n";
response << "Access-Control-Allow-Origin: " << allowedCorsOrigin() << "\r\n";
response << "Access-Control-Allow-Headers: Content-Type, 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);
};
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);
}
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_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 size_t max_body_size = is_photo_upload ? MAX_PHOTO_BODY_SIZE : MAX_JSON_BODY_SIZE;
if (content_length > max_body_size) {
response_body = makeJsonError("Request body too large");
status_code = 413;
status_text = "Payload Too Large";
sendResponse();
return;
}
size_t header_end = request.find("\r\n\r\n");
if (header_end != std::string::npos) {
size_t body_start = header_end + 4;
size_t current_body_size = request.size() - body_start;
if (current_body_size >= content_length) {
request_body = request.substr(body_start, content_length);
} else {
request_body = request.substr(body_start);
size_t remaining = content_length - current_body_size;
while (remaining > 0) {
char extra_buffer[BUFFER_SIZE] = {0};
int extra_bytes = recv(client_socket, extra_buffer,
static_cast<int>(std::min(remaining, (size_t)BUFFER_SIZE)), 0);
if (extra_bytes <= 0) break;
request_body.append(extra_buffer, extra_bytes);
remaining -= extra_bytes;
}
}
}
std::cout << "[HTTP] POST body size: " << request_body.size()
<< " bytes (expected: " << content_length << ")" << std::endl;
if (request_body.size() != content_length) {
response_body = makeJsonError("Incomplete request body");
status_code = 400;
status_text = "Bad Request";
sendResponse();
return;
}
}
// ============= 路由处理 =============
if (method == "OPTIONS") {
response_body = "{}";
} else if (method == "GET" && endpoint == "/api/v1/sync/version") {
response_body = makeSyncVersionResponse();
std::cout << "[SYNC] Version fetched: " << readSyncVersion() << std::endl;
} else if (method == "GET" && endpoint == "/api/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 encrypted checkin data..." << std::endl;
// 检查是否是加密数据
if (request.find("X-Encrypted: AES128-CBC") != std::string::npos) {
std::cout << "[CHECKIN] Encrypted data detected" << std::endl;
// 解密数据
std::string decrypted_json = decryptJSON(request_body);
if (decrypted_json.empty()) {
response_body = "{\"success\":false,\"message\":\"解密失败\"}";
status_code = 400;
status_text = "Bad Request";
} else {
// 解析JSON
auto json_data = parseJSON(decrypted_json);
if (json_data.find("student_id") != json_data.end()) {
// 构建考勤记录
std::string timestamp = getCurrentTimestamp();
std::string student_id = json_data["student_id"]; // K210已发送真实学号
std::string name = json_data.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;
// 保存到SQLite(包含班级编号)
if (saveAttendanceRecord(timestamp, student_id, name, confidence, status, class_id)) {
response_body = "{\"success\":true,\"message\":\"签到成功\",\"timestamp\":\""
+ jsonEscape(timestamp) + "\"}";
bumpSyncVersion("checkin");
std::cout << "[CHECKIN] ✓ " << name << " (ID:" << student_id << ", Class:" << class_id << ")" << std::endl;
} else {
response_body = "{\"success\":false,\"message\":\"数据库写入失败\"}";
status_code = 500;
status_text = "Internal Server Error";
}
} else {
response_body = "{\"success\":false,\"message\":\"缺少必要字段\"}";
status_code = 400;
status_text = "Bad Request";
}
}
} else {
response_body = "{\"success\":false,\"message\":\"未加密数据\"}";
status_code = 400;
status_text = "Bad Request";
}
}
// API端点:返回加密数据
else if (method == "GET" && endpoint == "/api/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 {
std::string json_data = readDatabase(limit, offset);
if (hasHeaderToken(request, "X-Encrypted-Response", "true")) {
response_body = encryptJSON(json_data);
if (response_body.empty()) {
response_body = makeJsonError("Encryption failed");
status_code = 500;
status_text = "Internal Server Error";
}
} else {
response_body = json_data;
}
}
} else if (method == "GET" && endpoint == "/api/v1/attendance/statistics") {
std::string json_data = getStatistics();
if (hasHeaderToken(request, "X-Encrypted-Response", "true")) {
response_body = encryptJSON(json_data);
if (response_body.empty()) {
response_body = makeJsonError("Encryption failed");
status_code = 500;
status_text = "Internal Server Error";
}
} else {
response_body = json_data;
}
} else if (method == "POST" && endpoint == "/api/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 == "POST" && endpoint == "/api/v1/config/mappings") {
// 上传映射配置: POST /api/v1/config/mappings
std::cout << "[MAPPING] Received upload request, data size: " << request_body.size() << " bytes" << std::endl;
if (saveMappingConfig(request_body)) {
response_body = "{\"success\":true,\"message\":\"映射配置已保存\"}";
bumpSyncVersion("mapping_config");
std::cout << "[MAPPING] ✓ Configuration saved successfully" << std::endl;
} else {
response_body = "{\"success\":false,\"message\":\"保存映射配置失败\"}";
status_code = 500;
status_text = "Internal Server Error";
}
} else if (method == "GET" && endpoint == "/api/v1/config/mappings") {
// 获取映射配置: GET /api/v1/config/mappings
std::cout << "[MAPPING] Reading configuration" << std::endl;
response_body = readMappingConfig();
std::cout << "[MAPPING] Returning config: " << response_body.size() << " bytes" << std::endl;
// ============= 人脸照片管理 API =============
} else if (method == "GET" && endpoint == "/api/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 == "POST" && endpoint == "/api/v1/faces/mappings") {
// 保存人脸ID映射
std::cout << "[FACE_MAPPING] Saving mapping, size: " << request_body.size() << " bytes" << std::endl;
if (saveFaceIdMapping(request_body)) {
bumpSyncVersion("face_id_mapping");
setSyncRequest(true);
std::cout << "[FACE_MAPPING] Auto-triggered K210 sync" << std::endl;
response_body = "{\"success\":true,\"message\":\"Face ID mapping saved, K210 sync triggered\"}";
} else {
response_body = "{\"success\":false,\"message\":\"Failed to save mapping\"}";
status_code = 500;
status_text = "Internal Server Error";
}
} else if (method == "GET" && endpoint == "/api/v1/faces/mappings") {
// 获取人脸ID映射
response_body = readFaceIdMapping();
std::cout << "[FACE_MAPPING] Mapping fetched" << std::endl;
} else if (method == "GET" && endpoint == "/api/v1/faces/student-ids") {
// 获取所有学生ID(从考勤记录提取)
response_body = getAllStudentIds();
std::cout << "[STUDENT_IDS] All student IDs fetched" << std::endl;
} else if (method == "DELETE" && endpoint.find("/api/v1/faces/") == 0) {
// 删除人脸信息: DELETE /api/v1/faces/<student_id>?delete_records=true
std::string student_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 student_id: " << student_id
<< ", delete_records: " << (delete_records ? "yes" : "no") << std::endl;
if (!isSafeId(student_id)) {
response_body = "{\"success\":false,\"message\":\"Invalid student_id\"}";
status_code = 400;
status_text = "Bad Request";
} else {
const std::set<std::string> k210_face_ids = resolveK210FaceIdsForDelete(student_id);
const std::set<std::string> student_ids = resolveStudentIdsForDelete(student_id);
bool photo_deleted = deleteFacePhoto(student_id);
bool mapping_deleted = removeFaceIdFromMapping(student_id);
int records_deleted = 0;
if (delete_records) {
for (const auto& id : student_ids) {
records_deleted += deleteRecordsByStudentId(id);
}
}
// 添加K210原始face_id到待删除列表,等待K210同步删除本地特征
bool pending_added = false;
for (const auto& face_id : k210_face_ids) {
if (face_id != student_id) {
photo_deleted = deleteFacePhoto(face_id) || photo_deleted;
mapping_deleted = removeFaceIdFromMapping(face_id) || mapping_deleted;
}
pending_added = addToPendingDelete(face_id, student_id) || pending_added;
}
for (const auto& id : student_ids) {
photo_deleted = deleteFacePhoto(id) || photo_deleted;
}
bumpSyncVersion("delete_face");
setSyncRequest(true);
std::ostringstream result;
result << "{\"success\":true,"
<< "\"photo_deleted\":" << (photo_deleted ? "true" : "false") << ","
<< "\"mapping_deleted\":" << (mapping_deleted ? "true" : "false") << ","
<< "\"records_deleted\":" << records_deleted << ","
<< "\"k210_pending\":" << (pending_added ? "true" : "false") << ","
<< "\"sync_requested\":true}";
response_body = result.str();
std::cout << "[DELETE_FACE] Result: " << response_body << std::endl;
}
} else if (method == "GET" && endpoint == "/api/v1/faces/pending-delete") {
// K210查询待删除列表
response_body = getPendingDeleteList();
std::cout << "[PENDING_DELETE] List fetched" << std::endl;
} else if (method == "POST" && endpoint == "/api/v1/faces/delete-ack") {
// K210确认删除完成,从待删除列表移除
// 请求体格式: {"face_id":"xxx","task_id":"xxx"}
auto ack_json = parseJSON(request_body);
std::string ack_id = ack_json.count("task_id") ? ack_json["task_id"] : "";
if (ack_id.empty() && ack_json.count("face_id")) {
ack_id = ack_json["face_id"];
}
if (!ack_id.empty() && removeFromPendingDelete(ack_id)) {
response_body = "{\"success\":true,\"message\":\"Confirmed delete: " + jsonEscape(ack_id) + "\"}";
bumpSyncVersion("confirm_delete");
std::cout << "[PENDING_DELETE] K210 confirmed delete: " << ack_id << std::endl;
} else {
response_body = "{\"success\":false,\"message\":\"Invalid face_id or not in pending list\"}";
status_code = 400;
status_text = "Bad Request";
}
// ============= K210同步请求 API =============
} else if (method == "GET" && endpoint == "/api/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 if (method == "POST" && endpoint == "/api/v1/sync/trigger") {
// QT端触发K210同步
if (setSyncRequest(true)) {
response_body = "{\"success\":true,\"message\":\"Sync request sent to K210\"}";
std::cout << "[SYNC] QT triggered K210 sync" << std::endl;
} else {
response_body = "{\"success\":false,\"message\":\"Failed to set sync request\"}";
status_code = 500;
status_text = "Internal Server Error";
}
} else {
response_body = "{\"error\":\"Not Found\"}";
status_code = 404;
status_text = "Not Found";
}
sendResponse();
}
// ============= HTTP服务器线程 =============
void httpServerThread() {
SOCKET server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == INVALID_SOCKET) {
std::cerr << "[HTTP] Failed to create socket" << std::endl;
return;
}
int opt = 1;
setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
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;
}