3067 lines
114 KiB
C++
3067 lines
114 KiB
C++
/*
|
||
* 增强版考勤服务器 - 支持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(<m, &now);
|
||
#else
|
||
localtime_r(&now, <m);
|
||
#endif
|
||
char buffer[80];
|
||
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", <m);
|
||
return std::string(buffer);
|
||
}
|
||
|
||
// 清理字符串中的控制字符(\r, \n等)
|
||
std::string cleanString(const std::string& str) {
|
||
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;
|
||
}
|