1745 lines
62 KiB
C++
1745 lines
62 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 -std=c++11
|
||
*/
|
||
|
||
#include <iostream>
|
||
#include <string>
|
||
#include <fstream>
|
||
#include <sstream>
|
||
#include <thread>
|
||
#include <vector>
|
||
#include <map>
|
||
#include <set>
|
||
#include <ctime>
|
||
#include <algorithm>
|
||
|
||
#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 <dirent.h>
|
||
#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>
|
||
|
||
const int HTTP_PORT = 8082;
|
||
const int BUFFER_SIZE = 4096;
|
||
const std::string LOG_FILE = "attendance.log";
|
||
const std::string DB_FILE = "attendance_database.csv";
|
||
const std::string TIMESLOTS_FILE = "timeslots_config.json"; // 时间段配置文件
|
||
const std::string MAPPING_CONFIG_FILE = "mapping_config.json"; // 映射配置文件
|
||
const std::string FACE_ID_MAPPING_FILE = "face_id_mapping.json"; // 人脸ID映射文件
|
||
const std::string FACES_DIR = "faces"; // 人脸照片目录
|
||
const std::string PENDING_DELETE_FILE = "pending_delete.json"; // 待删除人脸列表
|
||
const std::string SYNC_REQUEST_FILE = "sync_request.json"; // K210同步请求标志文件
|
||
|
||
// ⚠️ 重要:密钥和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;
|
||
}
|
||
|
||
// 简单的JSON解析
|
||
std::map<std::string, std::string> parseJSON(const std::string& json) {
|
||
std::map<std::string, std::string> result;
|
||
|
||
std::string content = json;
|
||
size_t start = content.find('{');
|
||
size_t end = content.rfind('}');
|
||
if (start != std::string::npos && end != std::string::npos) {
|
||
content = content.substr(start + 1, end - start - 1);
|
||
}
|
||
|
||
std::istringstream ss(content);
|
||
std::string token;
|
||
|
||
while (std::getline(ss, token, ',')) {
|
||
size_t colon = token.find(':');
|
||
if (colon != std::string::npos) {
|
||
std::string key = token.substr(0, colon);
|
||
std::string value = token.substr(colon + 1);
|
||
|
||
// 移除引号和空格
|
||
key.erase(std::remove_if(key.begin(), key.end(),
|
||
[](char c) { return c == '\"' || c == ' ' || c == '\t'; }), key.end());
|
||
value.erase(std::remove_if(value.begin(), value.end(),
|
||
[](char c) { return c == '\"'; }), value.end());
|
||
|
||
// 保留timestamp中的空格
|
||
if (key != "timestamp") {
|
||
value.erase(std::remove(value.begin(), value.end(), ' '), value.end());
|
||
}
|
||
|
||
result[key] = value;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
// ============= 数据库操作(与原server_with_api.cpp相同)=============
|
||
std::string readDatabase(int limit = -1, int offset = 0) {
|
||
std::ifstream db(DB_FILE);
|
||
if (!db.is_open()) {
|
||
return "[]";
|
||
}
|
||
|
||
std::vector<std::string> lines;
|
||
std::string line;
|
||
std::getline(db, line); // 跳过表头
|
||
|
||
while (std::getline(db, line)) {
|
||
if (!line.empty()) {
|
||
lines.push_back(line);
|
||
}
|
||
}
|
||
db.close();
|
||
|
||
std::reverse(lines.begin(), lines.end());
|
||
|
||
std::ostringstream json;
|
||
json << "[";
|
||
|
||
int start = offset;
|
||
int end = (limit > 0) ? std::min(start + limit, (int)lines.size()) : lines.size();
|
||
|
||
for (int i = start; i < end; ++i) {
|
||
if (i > start) json << ",";
|
||
|
||
std::istringstream ss(lines[i]);
|
||
std::string timestamp, student_id, name, conf, status, class_id;
|
||
|
||
std::getline(ss, timestamp, ',');
|
||
std::getline(ss, student_id, ',');
|
||
std::getline(ss, name, ',');
|
||
std::getline(ss, conf, ',');
|
||
std::getline(ss, status, ',');
|
||
std::getline(ss, class_id, ',');
|
||
|
||
// 兼容旧数据:如果没有class_id字段,默认为"0"
|
||
if (class_id.empty()) class_id = "0";
|
||
|
||
json << "{"
|
||
<< "\"timestamp\":\"" << timestamp << "\","
|
||
<< "\"student_id\":\"" << student_id << "\","
|
||
<< "\"name\":\"" << name << "\","
|
||
<< "\"confidence\":" << conf << ","
|
||
<< "\"status\":\"" << status << "\","
|
||
<< "\"class_id\":\"" << class_id << "\""
|
||
<< "}";
|
||
}
|
||
|
||
json << "]";
|
||
return json.str();
|
||
}
|
||
|
||
std::string getStatistics() {
|
||
std::ifstream db(DB_FILE);
|
||
if (!db.is_open()) {
|
||
return "{\"total\":0,\"today\":0}";
|
||
}
|
||
|
||
std::string line;
|
||
std::getline(db, line);
|
||
|
||
int total = 0;
|
||
int today = 0;
|
||
std::string todayDate = getCurrentTimestamp().substr(0, 10);
|
||
|
||
while (std::getline(db, line)) {
|
||
if (!line.empty()) {
|
||
total++;
|
||
if (line.find(todayDate) == 0) {
|
||
today++;
|
||
}
|
||
}
|
||
}
|
||
db.close();
|
||
|
||
std::ostringstream json;
|
||
json << "{"
|
||
<< "\"total\":" << total << ","
|
||
<< "\"today\":" << today << ","
|
||
<< "\"server_time\":\"" << getCurrentTimestamp() << "\""
|
||
<< "}";
|
||
return json.str();
|
||
}
|
||
|
||
bool clearDatabase() {
|
||
std::ofstream db(DB_FILE);
|
||
if (!db.is_open()) return false;
|
||
db << "Timestamp,Student_ID,Name,Confidence,Status\n";
|
||
db.close();
|
||
return true;
|
||
}
|
||
|
||
// ============= 时间段配置管理 =============
|
||
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;
|
||
}
|
||
|
||
std::ofstream file(TIMESLOTS_FILE);
|
||
if (!file.is_open()) {
|
||
std::cerr << "[TIMESLOTS] Failed to open file for writing: " << TIMESLOTS_FILE << std::endl;
|
||
return false;
|
||
}
|
||
|
||
file << json_data;
|
||
file.flush();
|
||
file.close();
|
||
|
||
std::cout << "[TIMESLOTS] ✓ Configuration saved successfully to " << TIMESLOTS_FILE << std::endl;
|
||
std::cout << "[TIMESLOTS] Preview: " << json_data.substr(0, 100) << "..." << std::endl;
|
||
return true;
|
||
}
|
||
|
||
std::string readTimeSlots() {
|
||
std::ifstream file(TIMESLOTS_FILE);
|
||
if (!file.is_open()) {
|
||
std::cout << "[TIMESLOTS] File not found, returning default config" << std::endl;
|
||
return "{\"time_slots\":[]}";
|
||
}
|
||
|
||
std::stringstream buffer;
|
||
buffer << file.rdbuf();
|
||
file.close();
|
||
|
||
std::string content = buffer.str();
|
||
std::cout << "[TIMESLOTS] Read " << content.size() << " bytes from config file" << std::endl;
|
||
return content;
|
||
}
|
||
|
||
// ============= 检查重复签到 =============
|
||
// 参数: 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::ifstream db(DB_FILE);
|
||
if (!db.is_open()) {
|
||
return "{\"duplicate\":false}";
|
||
}
|
||
|
||
// 获取今天的日期
|
||
std::string todayDate = getCurrentTimestamp().substr(0, 10);
|
||
|
||
// 解析时段开始和结束时间
|
||
int start_hour = 0, start_minute = 0, end_hour = 23, end_minute = 59;
|
||
if (slot_start.length() >= 5) {
|
||
start_hour = std::stoi(slot_start.substr(0, 2));
|
||
start_minute = std::stoi(slot_start.substr(3, 2));
|
||
}
|
||
if (slot_end.length() >= 5) {
|
||
end_hour = std::stoi(slot_end.substr(0, 2));
|
||
end_minute = std::stoi(slot_end.substr(3, 2));
|
||
}
|
||
|
||
std::string line;
|
||
std::getline(db, line); // 跳过表头
|
||
|
||
while (std::getline(db, line)) {
|
||
if (line.empty()) continue;
|
||
|
||
// 解析CSV: Timestamp,Student_ID,Name,Confidence,Status
|
||
std::istringstream ss(line);
|
||
std::string timestamp, record_student_id, name, conf, status;
|
||
|
||
std::getline(ss, timestamp, ',');
|
||
std::getline(ss, record_student_id, ',');
|
||
std::getline(ss, name, ',');
|
||
std::getline(ss, conf, ',');
|
||
std::getline(ss, status, ',');
|
||
|
||
// 检查是否是同一个学生
|
||
if (record_student_id != student_id) continue;
|
||
|
||
// 检查是否是今天的记录
|
||
if (timestamp.substr(0, 10) != todayDate) continue;
|
||
|
||
// 解析记录的时间 (格式: YYYY-MM-DD HH:MM:SS)
|
||
if (timestamp.length() >= 16) {
|
||
int record_hour = std::stoi(timestamp.substr(11, 2));
|
||
int record_minute = std::stoi(timestamp.substr(14, 2));
|
||
int record_total_minutes = record_hour * 60 + record_minute;
|
||
int start_total_minutes = start_hour * 60 + start_minute;
|
||
int end_total_minutes = end_hour * 60 + end_minute;
|
||
|
||
// 检查记录是否在指定时段内
|
||
if (record_total_minutes >= start_total_minutes && record_total_minutes <= end_total_minutes) {
|
||
db.close();
|
||
std::cout << "[CHECK] Duplicate found: " << student_id << " at " << timestamp << std::endl;
|
||
return "{\"duplicate\":true,\"record_time\":\"" + timestamp + "\"}";
|
||
}
|
||
}
|
||
}
|
||
|
||
db.close();
|
||
return "{\"duplicate\":false}";
|
||
}
|
||
|
||
// ============= 删除记录 =============
|
||
bool deleteRecord(const std::string& timestamp, const std::string& student_id) {
|
||
std::ifstream inFile(DB_FILE);
|
||
if (!inFile.is_open()) return false;
|
||
|
||
std::vector<std::string> lines;
|
||
std::string line;
|
||
|
||
std::getline(inFile, line);
|
||
lines.push_back(line);
|
||
|
||
bool found = false;
|
||
while (std::getline(inFile, line)) {
|
||
if (line.empty()) continue;
|
||
|
||
if (line.find(timestamp) == 0 && line.find(student_id) != std::string::npos) {
|
||
found = true;
|
||
std::cout << "[DELETE] 删除记录: " << line << std::endl;
|
||
continue;
|
||
}
|
||
|
||
lines.push_back(line);
|
||
}
|
||
inFile.close();
|
||
|
||
if (!found) return false;
|
||
|
||
std::ofstream outFile(DB_FILE);
|
||
if (!outFile.is_open()) return false;
|
||
|
||
for (const auto& l : lines) {
|
||
outFile << l << "\n";
|
||
}
|
||
outFile.close();
|
||
|
||
return true;
|
||
}
|
||
|
||
// ============= 人脸照片管理函数 =============
|
||
|
||
// 保存人脸ID映射配置
|
||
bool saveFaceIdMapping(const std::string& json_data) {
|
||
std::cout << "[FACE_MAPPING] Saving " << json_data.size() << " bytes" << std::endl;
|
||
std::ofstream file(FACE_ID_MAPPING_FILE);
|
||
if (!file.is_open()) return false;
|
||
file << json_data;
|
||
file.close();
|
||
return true;
|
||
}
|
||
|
||
// 根据映射重命名照片文件
|
||
void renameFacePhotos(const std::string& json_data) {
|
||
std::cout << "[FACE_PHOTO] Renaming photos based on mapping..." << std::endl;
|
||
|
||
// 解析JSON: {"mappings":{"1":"20210001","2":"20210002"}}
|
||
size_t mappings_start = json_data.find("\"mappings\":{");
|
||
if (mappings_start == std::string::npos) return;
|
||
|
||
mappings_start += 12; // 跳过 "mappings":{
|
||
size_t mappings_end = json_data.find("}", mappings_start);
|
||
if (mappings_end == std::string::npos) return;
|
||
|
||
std::string mappings_str = json_data.substr(mappings_start, mappings_end - mappings_start);
|
||
|
||
// 解析每个映射对
|
||
std::map<std::string, std::string> mappings;
|
||
size_t pos = 0;
|
||
while (pos < mappings_str.length()) {
|
||
// 找到键
|
||
size_t key_start = mappings_str.find("\"", pos);
|
||
if (key_start == std::string::npos) break;
|
||
size_t key_end = mappings_str.find("\"", key_start + 1);
|
||
if (key_end == std::string::npos) break;
|
||
|
||
// 找到值
|
||
size_t val_start = mappings_str.find("\"", key_end + 1);
|
||
if (val_start == std::string::npos) break;
|
||
size_t val_end = mappings_str.find("\"", val_start + 1);
|
||
if (val_end == std::string::npos) break;
|
||
|
||
std::string key = mappings_str.substr(key_start + 1, key_end - key_start - 1);
|
||
std::string value = mappings_str.substr(val_start + 1, val_end - val_start - 1);
|
||
|
||
if (!key.empty() && !value.empty()) {
|
||
mappings[key] = value;
|
||
}
|
||
|
||
pos = val_end + 1;
|
||
}
|
||
|
||
// 遍历faces目录,重命名匹配的照片
|
||
std::vector<std::pair<std::string, std::string>> rename_list;
|
||
|
||
#ifdef _WIN32
|
||
WIN32_FIND_DATAA findData;
|
||
std::string searchPath = FACES_DIR + "\\*.jpg";
|
||
HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData);
|
||
if (hFind != INVALID_HANDLE_VALUE) {
|
||
do {
|
||
std::string filename = findData.cFileName;
|
||
std::string base_name = filename.substr(0, filename.length() - 4); // 去掉.jpg
|
||
|
||
// 检查是否以某个face_id开头
|
||
for (const auto& m : mappings) {
|
||
// 匹配 "face_id.jpg" 或 "face_id_xxx.jpg"
|
||
if (base_name == m.first ||
|
||
(base_name.find(m.first + "_") == 0)) {
|
||
// 直接使用学号作为新文件名(不保留时间戳)
|
||
std::string new_filename = m.second + ".jpg";
|
||
|
||
if (filename != new_filename) {
|
||
rename_list.push_back({filename, new_filename});
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
} while (FindNextFileA(hFind, &findData));
|
||
FindClose(hFind);
|
||
}
|
||
#else
|
||
DIR* dir = opendir(FACES_DIR.c_str());
|
||
if (dir) {
|
||
struct dirent* entry;
|
||
while ((entry = readdir(dir)) != NULL) {
|
||
std::string filename = entry->d_name;
|
||
if (filename.size() > 4 && filename.substr(filename.size() - 4) == ".jpg") {
|
||
std::string base_name = filename.substr(0, filename.length() - 4);
|
||
|
||
for (const auto& m : mappings) {
|
||
if (base_name == m.first ||
|
||
(base_name.find(m.first + "_") == 0)) {
|
||
std::string new_filename = m.second + ".jpg";
|
||
|
||
if (filename != new_filename) {
|
||
rename_list.push_back({filename, new_filename});
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
closedir(dir);
|
||
}
|
||
#endif
|
||
|
||
// 执行重命名
|
||
for (const auto& r : rename_list) {
|
||
std::string old_path = FACES_DIR + "/" + r.first;
|
||
std::string new_path = FACES_DIR + "/" + r.second;
|
||
|
||
// 如果目标文件已存在,先删除旧照片
|
||
std::remove(new_path.c_str());
|
||
|
||
if (std::rename(old_path.c_str(), new_path.c_str()) == 0) {
|
||
std::cout << "[FACE_PHOTO] Renamed: " << r.first << " -> " << r.second << std::endl;
|
||
} else {
|
||
std::cerr << "[FACE_PHOTO] Rename failed: " << r.first << std::endl;
|
||
}
|
||
}
|
||
|
||
std::cout << "[FACE_PHOTO] Renamed " << rename_list.size() << " photos" << std::endl;
|
||
}
|
||
|
||
// 读取人脸ID映射配置
|
||
std::string readFaceIdMapping() {
|
||
std::ifstream file(FACE_ID_MAPPING_FILE);
|
||
if (!file.is_open()) return "{\"mappings\":{}}";
|
||
std::stringstream buffer;
|
||
buffer << file.rdbuf();
|
||
file.close();
|
||
std::string content = buffer.str();
|
||
return content.empty() ? "{\"mappings\":{}}" : content;
|
||
}
|
||
|
||
// 获取人脸照片列表
|
||
std::string getFacePhotosList() {
|
||
std::ostringstream json;
|
||
json << "{\"photos\":[";
|
||
|
||
std::vector<std::string> photos;
|
||
|
||
#ifdef _WIN32
|
||
WIN32_FIND_DATAA findData;
|
||
std::string searchPath = FACES_DIR + "\\*.jpg";
|
||
HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData);
|
||
if (hFind != INVALID_HANDLE_VALUE) {
|
||
do {
|
||
photos.push_back(findData.cFileName);
|
||
} while (FindNextFileA(hFind, &findData));
|
||
FindClose(hFind);
|
||
}
|
||
#else
|
||
DIR* dir = opendir(FACES_DIR.c_str());
|
||
if (dir) {
|
||
struct dirent* entry;
|
||
while ((entry = readdir(dir)) != NULL) {
|
||
std::string name = entry->d_name;
|
||
if (name.size() > 4 && name.substr(name.size() - 4) == ".jpg") {
|
||
photos.push_back(name);
|
||
}
|
||
}
|
||
closedir(dir);
|
||
}
|
||
#endif
|
||
|
||
std::sort(photos.begin(), photos.end());
|
||
|
||
for (size_t i = 0; i < photos.size(); ++i) {
|
||
if (i > 0) json << ",";
|
||
std::string id = photos[i].substr(0, photos[i].size() - 4);
|
||
json << "{\"id\":\"" << id << "\",\"filename\":\"" << photos[i] << "\"}";
|
||
}
|
||
|
||
json << "]}";
|
||
return json.str();
|
||
}
|
||
|
||
// 保存人脸照片
|
||
bool saveFacePhoto(const std::string& face_id, const std::string& photo_data) {
|
||
_mkdir(FACES_DIR.c_str());
|
||
std::string filepath = FACES_DIR + "/" + face_id + ".jpg";
|
||
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();
|
||
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 = FACES_DIR + "/" + face_id + ".jpg";
|
||
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 = FACES_DIR + "/" + face_id + ".jpg";
|
||
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) {
|
||
std::ifstream inFile(FACE_ID_MAPPING_FILE);
|
||
if (!inFile.is_open()) return false;
|
||
|
||
std::stringstream buffer;
|
||
buffer << inFile.rdbuf();
|
||
inFile.close();
|
||
std::string content = buffer.str();
|
||
|
||
// 简单的JSON处理:移除 "face_id":"xxx" 条目
|
||
std::string search_pattern1 = "\"" + face_id + "\":\"";
|
||
size_t pos = content.find(search_pattern1);
|
||
if (pos != std::string::npos) {
|
||
// 找到值的结束位置
|
||
size_t value_start = pos + search_pattern1.length();
|
||
size_t value_end = content.find("\"", value_start);
|
||
if (value_end != std::string::npos) {
|
||
// 检查是否需要删除前面的逗号
|
||
size_t delete_start = pos;
|
||
size_t delete_end = value_end + 1;
|
||
|
||
// 检查后面是否有逗号
|
||
if (delete_end < content.length() && content[delete_end] == ',') {
|
||
delete_end++;
|
||
} else if (delete_start > 0 && content[delete_start - 1] == ',') {
|
||
delete_start--;
|
||
}
|
||
|
||
content = content.substr(0, delete_start) + content.substr(delete_end);
|
||
|
||
// 保存修改后的内容
|
||
std::ofstream outFile(FACE_ID_MAPPING_FILE);
|
||
if (outFile.is_open()) {
|
||
outFile << content;
|
||
outFile.close();
|
||
std::cout << "[FACE_MAPPING] Removed face_id: " << face_id << std::endl;
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 删除指定学生ID的所有考勤记录
|
||
int deleteRecordsByStudentId(const std::string& student_id) {
|
||
std::ifstream inFile(DB_FILE);
|
||
if (!inFile.is_open()) return 0;
|
||
|
||
std::vector<std::string> lines;
|
||
std::string line;
|
||
|
||
// 读取表头
|
||
std::getline(inFile, line);
|
||
lines.push_back(line);
|
||
|
||
int deleted_count = 0;
|
||
while (std::getline(inFile, line)) {
|
||
if (line.empty()) continue;
|
||
|
||
// 解析CSV找到student_id字段
|
||
std::istringstream ss(line);
|
||
std::string timestamp, record_student_id;
|
||
std::getline(ss, timestamp, ',');
|
||
std::getline(ss, record_student_id, ',');
|
||
|
||
if (record_student_id == student_id) {
|
||
deleted_count++;
|
||
std::cout << "[DELETE] 删除考勤记录: " << line << std::endl;
|
||
continue;
|
||
}
|
||
|
||
lines.push_back(line);
|
||
}
|
||
inFile.close();
|
||
|
||
if (deleted_count == 0) return 0;
|
||
|
||
std::ofstream outFile(DB_FILE);
|
||
if (!outFile.is_open()) return 0;
|
||
|
||
for (const auto& l : lines) {
|
||
outFile << l << "\n";
|
||
}
|
||
outFile.close();
|
||
|
||
return deleted_count;
|
||
}
|
||
|
||
// 获取所有学生ID(从考勤记录提取,包括没有照片的)
|
||
std::string getAllStudentIds() {
|
||
std::set<std::string> student_ids;
|
||
|
||
// 从考勤记录中提取所有学生ID
|
||
std::ifstream db(DB_FILE);
|
||
if (db.is_open()) {
|
||
std::string line;
|
||
std::getline(db, line); // 跳过表头
|
||
|
||
while (std::getline(db, line)) {
|
||
if (line.empty()) continue;
|
||
|
||
std::istringstream ss(line);
|
||
std::string timestamp, student_id;
|
||
std::getline(ss, timestamp, ',');
|
||
std::getline(ss, student_id, ',');
|
||
|
||
if (!student_id.empty()) {
|
||
student_ids.insert(student_id);
|
||
}
|
||
}
|
||
db.close();
|
||
}
|
||
|
||
// 构建JSON响应
|
||
std::ostringstream json;
|
||
json << "{\"student_ids\":[";
|
||
|
||
bool first = true;
|
||
for (const auto& id : student_ids) {
|
||
if (!first) json << ",";
|
||
json << "\"" << id << "\"";
|
||
first = false;
|
||
}
|
||
|
||
json << "]}";
|
||
return json.str();
|
||
}
|
||
|
||
// 添加人脸ID到待删除列表
|
||
bool addToPendingDelete(const std::string& face_id) {
|
||
std::set<std::string> pending_ids;
|
||
|
||
// 读取现有列表
|
||
std::ifstream inFile(PENDING_DELETE_FILE);
|
||
if (inFile.is_open()) {
|
||
std::stringstream buffer;
|
||
buffer << inFile.rdbuf();
|
||
inFile.close();
|
||
std::string content = buffer.str();
|
||
|
||
// 简单解析JSON数组
|
||
size_t start = content.find('[');
|
||
size_t end = content.rfind(']');
|
||
if (start != std::string::npos && end != std::string::npos) {
|
||
std::string arr = content.substr(start + 1, end - start - 1);
|
||
std::istringstream ss(arr);
|
||
std::string token;
|
||
while (std::getline(ss, token, ',')) {
|
||
token.erase(std::remove(token.begin(), token.end(), '"'), token.end());
|
||
token.erase(std::remove(token.begin(), token.end(), ' '), token.end());
|
||
if (!token.empty()) {
|
||
pending_ids.insert(token);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加新ID
|
||
pending_ids.insert(face_id);
|
||
|
||
// 写回文件
|
||
std::ofstream outFile(PENDING_DELETE_FILE);
|
||
if (!outFile.is_open()) return false;
|
||
|
||
outFile << "{\"pending_delete\":[";
|
||
bool first = true;
|
||
for (const auto& id : pending_ids) {
|
||
if (!first) outFile << ",";
|
||
outFile << "\"" << id << "\"";
|
||
first = false;
|
||
}
|
||
outFile << "]}";
|
||
outFile.close();
|
||
|
||
std::cout << "[PENDING_DELETE] Added: " << face_id << std::endl;
|
||
return true;
|
||
}
|
||
|
||
// 获取待删除列表
|
||
std::string getPendingDeleteList() {
|
||
std::ifstream file(PENDING_DELETE_FILE);
|
||
if (!file.is_open()) {
|
||
return "{\"pending_delete\":[]}";
|
||
}
|
||
std::stringstream buffer;
|
||
buffer << file.rdbuf();
|
||
file.close();
|
||
std::string content = buffer.str();
|
||
return content.empty() ? "{\"pending_delete\":[]}" : content;
|
||
}
|
||
|
||
// 从待删除列表中移除指定ID(K210确认删除后调用)
|
||
bool removeFromPendingDelete(const std::string& face_id) {
|
||
std::set<std::string> pending_ids;
|
||
|
||
// 读取现有列表
|
||
std::ifstream inFile(PENDING_DELETE_FILE);
|
||
if (inFile.is_open()) {
|
||
std::stringstream buffer;
|
||
buffer << inFile.rdbuf();
|
||
inFile.close();
|
||
std::string content = buffer.str();
|
||
|
||
size_t start = content.find('[');
|
||
size_t end = content.rfind(']');
|
||
if (start != std::string::npos && end != std::string::npos) {
|
||
std::string arr = content.substr(start + 1, end - start - 1);
|
||
std::istringstream ss(arr);
|
||
std::string token;
|
||
while (std::getline(ss, token, ',')) {
|
||
token.erase(std::remove(token.begin(), token.end(), '"'), token.end());
|
||
token.erase(std::remove(token.begin(), token.end(), ' '), token.end());
|
||
if (!token.empty() && token != face_id) {
|
||
pending_ids.insert(token);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 写回文件
|
||
std::ofstream outFile(PENDING_DELETE_FILE);
|
||
if (!outFile.is_open()) return false;
|
||
|
||
outFile << "{\"pending_delete\":[";
|
||
bool first = true;
|
||
for (const auto& id : pending_ids) {
|
||
if (!first) outFile << ",";
|
||
outFile << "\"" << id << "\"";
|
||
first = false;
|
||
}
|
||
outFile << "]}";
|
||
outFile.close();
|
||
|
||
std::cout << "[PENDING_DELETE] Removed: " << face_id << std::endl;
|
||
return true;
|
||
}
|
||
|
||
// ============= K210同步请求管理 =============
|
||
// 设置同步请求标志(QT端调用)
|
||
bool setSyncRequest(bool sync) {
|
||
std::ofstream file(SYNC_REQUEST_FILE);
|
||
if (!file.is_open()) return false;
|
||
file << "{\"sync\":" << (sync ? "true" : "false") << "}";
|
||
file.close();
|
||
std::cout << "[SYNC] Sync request set to: " << (sync ? "true" : "false") << std::endl;
|
||
return true;
|
||
}
|
||
|
||
// 获取同步请求状态(K210轮询调用)
|
||
std::string getSyncRequest() {
|
||
std::ifstream file(SYNC_REQUEST_FILE);
|
||
if (!file.is_open()) {
|
||
return "{\"sync\":false}";
|
||
}
|
||
std::stringstream buffer;
|
||
buffer << file.rdbuf();
|
||
file.close();
|
||
std::string content = buffer.str();
|
||
return content.empty() ? "{\"sync\":false}" : content;
|
||
}
|
||
|
||
// 清除同步请求标志(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;
|
||
|
||
std::ifstream file(FACE_ID_MAPPING_FILE);
|
||
if (!file.is_open()) {
|
||
std::cout << "[MAPPING] File not found: " << FACE_ID_MAPPING_FILE << ", using original ID" << std::endl;
|
||
return face_id; // 没有映射文件,返回原ID
|
||
}
|
||
|
||
std::stringstream buffer;
|
||
buffer << file.rdbuf();
|
||
file.close();
|
||
std::string content = buffer.str();
|
||
|
||
std::cout << "[MAPPING] File content: " << content << std::endl;
|
||
|
||
// 简单解析JSON查找 "face_id":"student_id"
|
||
// 格式: {"mappings":{"1":"20210001","2":"20210002"}}
|
||
std::string search_key = "\"" + face_id + "\":\"";
|
||
size_t pos = content.find(search_key);
|
||
if (pos != std::string::npos) {
|
||
size_t start = pos + search_key.length();
|
||
size_t end = content.find("\"", start);
|
||
if (end != std::string::npos) {
|
||
std::string mapped_id = content.substr(start, end - start);
|
||
if (!mapped_id.empty()) {
|
||
std::cout << "[MAPPING] ✓ Face ID " << face_id << " -> Student ID " << mapped_id << std::endl;
|
||
return mapped_id;
|
||
}
|
||
}
|
||
}
|
||
|
||
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";
|
||
|
||
std::string endpoint = path;
|
||
std::string query;
|
||
size_t q_pos = path.find('?');
|
||
if (q_pos != std::string::npos) {
|
||
endpoint = path.substr(0, q_pos);
|
||
query = path.substr(q_pos + 1);
|
||
}
|
||
|
||
// 提取请求体
|
||
std::string request_body;
|
||
if (method == "POST") {
|
||
size_t content_length = 0;
|
||
size_t cl_pos = request.find("Content-Length:");
|
||
if (cl_pos != std::string::npos) {
|
||
size_t cl_end = request.find("\r\n", cl_pos);
|
||
std::string cl_str = request.substr(cl_pos + 15, cl_end - cl_pos - 15);
|
||
cl_str.erase(0, cl_str.find_first_not_of(" \t"));
|
||
content_length = std::stoi(cl_str);
|
||
}
|
||
|
||
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 && remaining < BUFFER_SIZE * 10) {
|
||
char extra_buffer[BUFFER_SIZE] = {0};
|
||
int extra_bytes = recv(client_socket, extra_buffer,
|
||
std::min(remaining, (size_t)BUFFER_SIZE - 1), 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 (method == "POST" && endpoint == "/api/checkin") {
|
||
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["name"];
|
||
std::string confidence = json_data["confidence"];
|
||
std::string status = json_data["status"];
|
||
std::string class_id = json_data.count("class_id") ? json_data["class_id"] : "0";
|
||
|
||
std::cout << "[CHECKIN] Student ID: " << student_id << std::endl;
|
||
|
||
// 保存到CSV(包含班级编号)
|
||
std::ofstream db(DB_FILE, std::ios::app);
|
||
if (db.is_open()) {
|
||
db << timestamp << ","
|
||
<< student_id << ","
|
||
<< name << ","
|
||
<< confidence << ","
|
||
<< status << ","
|
||
<< class_id << "\n";
|
||
db.close();
|
||
|
||
response_body = "{\"success\":true,\"message\":\"签到成功\",\"timestamp\":\""
|
||
+ timestamp + "\"}";
|
||
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/records") {
|
||
auto params = parseQueryParams(query);
|
||
int limit = params.count("limit") ? std::stoi(params["limit"]) : 100;
|
||
int offset = params.count("offset") ? std::stoi(params["offset"]) : 0;
|
||
|
||
std::string json_data = readDatabase(limit, offset);
|
||
|
||
// 🔒 强制加密所有响应(不再是可选)
|
||
response_body = encryptJSON(json_data);
|
||
if (response_body.empty()) {
|
||
response_body = "{\\\"error\\\":\\\"Encryption failed\\\"}";
|
||
status_code = 500;
|
||
status_text = "Internal Server Error";
|
||
}
|
||
|
||
} else if (method == "GET" && endpoint == "/api/statistics") {
|
||
std::string json_data = getStatistics();
|
||
|
||
// 🔒 强制加密所有响应
|
||
response_body = encryptJSON(json_data);
|
||
if (response_body.empty()) {
|
||
response_body = "{\\\"error\\\":\\\"Encryption failed\\\"}";
|
||
status_code = 500;
|
||
status_text = "Internal Server Error";
|
||
}
|
||
|
||
} else if (method == "POST" && endpoint == "/api/clear") {
|
||
if (clearDatabase()) {
|
||
response_body = "{\"success\":true,\"message\":\"Database cleared\"}";
|
||
} else {
|
||
response_body = "{\"success\":false,\"message\":\"Failed to clear database\"}";
|
||
status_code = 500;
|
||
status_text = "Internal Server Error";
|
||
}
|
||
|
||
} else if (method == "DELETE" && endpoint == "/api/record") {
|
||
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\"}";
|
||
} 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/health") {
|
||
response_body = "{\"status\":\"healthy\",\"time\":\"" + getCurrentTimestamp() + "\"}";
|
||
|
||
} else if (method == "GET" && endpoint == "/api/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/timeslots") {
|
||
// 上传时间段配置: POST /api/timeslots
|
||
std::cout << "[TIMESLOTS] Received upload request, data size: " << request_body.size() << " bytes" << std::endl;
|
||
|
||
if (saveTimeSlots(request_body)) {
|
||
response_body = "{\"success\":true,\"message\":\"时间段配置已更新\"}";
|
||
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/timeslots") {
|
||
// 获取时间段配置: GET /api/timeslots
|
||
std::cout << "[TIMESLOTS] Reading configuration" << std::endl;
|
||
response_body = readTimeSlots();
|
||
|
||
} else if (method == "GET" && endpoint == "/api/check_duplicate") {
|
||
// 检查重复签到: GET /api/check_duplicate?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/today_records") {
|
||
// 获取今日已签到记录(供STM32开机下载)
|
||
// GET /api/today_records?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::ifstream db(DB_FILE);
|
||
std::string todayDate = getCurrentTimestamp().substr(0, 10);
|
||
|
||
// 解析时段
|
||
int start_hour = 0, start_minute = 0, end_hour = 23, end_minute = 59;
|
||
if (slot_start.length() >= 5) {
|
||
start_hour = std::stoi(slot_start.substr(0, 2));
|
||
start_minute = std::stoi(slot_start.substr(3, 2));
|
||
}
|
||
if (slot_end.length() >= 5) {
|
||
end_hour = std::stoi(slot_end.substr(0, 2));
|
||
end_minute = std::stoi(slot_end.substr(3, 2));
|
||
}
|
||
|
||
std::ostringstream json;
|
||
json << "{\"date\":\"" << todayDate << "\",\"records\":[";
|
||
|
||
if (db.is_open()) {
|
||
std::string line;
|
||
std::getline(db, line); // 跳过表头
|
||
bool first = true;
|
||
|
||
while (std::getline(db, line)) {
|
||
if (line.empty()) continue;
|
||
|
||
// 检查是否是今天的记录
|
||
if (line.substr(0, 10) != todayDate) continue;
|
||
|
||
// 解析时间
|
||
if (line.length() >= 16) {
|
||
int record_hour = std::stoi(line.substr(11, 2));
|
||
int record_minute = std::stoi(line.substr(14, 2));
|
||
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) {
|
||
// 提取学号
|
||
std::istringstream ss(line);
|
||
std::string timestamp, student_id;
|
||
std::getline(ss, timestamp, ',');
|
||
std::getline(ss, student_id, ',');
|
||
|
||
if (!first) json << ",";
|
||
json << "\"" << student_id << "\"";
|
||
first = false;
|
||
}
|
||
}
|
||
}
|
||
db.close();
|
||
}
|
||
|
||
json << "]}";
|
||
response_body = json.str();
|
||
std::cout << "[TODAY] Returning today's records: " << response_body << std::endl;
|
||
|
||
} else if (method == "POST" && endpoint == "/api/mapping_config") {
|
||
// 上传映射配置: POST /api/mapping_config
|
||
std::cout << "[MAPPING] Received upload request, data size: " << request_body.size() << " bytes" << std::endl;
|
||
|
||
std::ofstream file(MAPPING_CONFIG_FILE);
|
||
if (file.is_open()) {
|
||
file << request_body;
|
||
file.flush();
|
||
file.close();
|
||
response_body = "{\"success\":true,\"message\":\"映射配置已保存\"}";
|
||
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/mapping_config") {
|
||
// 获取映射配置: GET /api/mapping_config
|
||
std::cout << "[MAPPING] Reading configuration" << std::endl;
|
||
|
||
std::ifstream file(MAPPING_CONFIG_FILE);
|
||
if (file.is_open()) {
|
||
std::stringstream buffer;
|
||
buffer << file.rdbuf();
|
||
file.close();
|
||
response_body = buffer.str();
|
||
std::cout << "[MAPPING] Returning config: " << response_body.size() << " bytes" << std::endl;
|
||
} else {
|
||
// 返回空配置
|
||
response_body = "{\"student_name_map\":{},\"class_name_map\":{}}";
|
||
std::cout << "[MAPPING] Config file not found, returning empty config" << std::endl;
|
||
}
|
||
|
||
// ============= 人脸照片管理 API =============
|
||
} else if (method == "GET" && endpoint == "/api/face_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/face_photo/") == 0) {
|
||
// 获取指定人脸照片: GET /api/face_photo/<id>
|
||
std::string face_id = endpoint.substr(16);
|
||
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/face_photo_simple/") == 0) {
|
||
// 简化版人脸照片上传: POST /api/face_photo_simple/<face_id>
|
||
std::string face_id = endpoint.substr(23);
|
||
std::cout << "[FACE_PHOTO] Simple upload for face_id: " << face_id << ", size: " << request_body.size() << " bytes" << std::endl;
|
||
|
||
if (!face_id.empty() && !request_body.empty()) {
|
||
if (saveFacePhoto(face_id, request_body)) {
|
||
response_body = "{\"success\":true,\"message\":\"Photo saved\",\"face_id\":\"" + face_id + "\"}";
|
||
} 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/face_id_mapping") {
|
||
// 保存人脸ID映射
|
||
std::cout << "[FACE_MAPPING] Saving mapping, size: " << request_body.size() << " bytes" << std::endl;
|
||
if (saveFaceIdMapping(request_body)) {
|
||
// 根据新映射重命名照片文件
|
||
renameFacePhotos(request_body);
|
||
// 自动触发K210同步
|
||
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/face_id_mapping") {
|
||
// 获取人脸ID映射
|
||
response_body = readFaceIdMapping();
|
||
std::cout << "[FACE_MAPPING] Mapping fetched" << std::endl;
|
||
|
||
} else if (method == "GET" && endpoint == "/api/all_student_ids") {
|
||
// 获取所有学生ID(从考勤记录提取)
|
||
response_body = getAllStudentIds();
|
||
std::cout << "[STUDENT_IDS] All student IDs fetched" << std::endl;
|
||
|
||
} else if (method == "DELETE" && endpoint.find("/api/face/") == 0) {
|
||
// 删除人脸信息: DELETE /api/face/<student_id>?delete_records=true
|
||
std::string student_id = endpoint.substr(10); // 去掉 "/api/face/"
|
||
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;
|
||
|
||
bool photo_deleted = deleteFacePhoto(student_id);
|
||
bool mapping_deleted = removeFaceIdFromMapping(student_id);
|
||
int records_deleted = 0;
|
||
|
||
if (delete_records) {
|
||
records_deleted = deleteRecordsByStudentId(student_id);
|
||
}
|
||
|
||
// 添加到待删除列表,等待K210同步删除
|
||
addToPendingDelete(student_id);
|
||
|
||
std::ostringstream result;
|
||
result << "{\"success\":true,"
|
||
<< "\"photo_deleted\":" << (photo_deleted ? "true" : "false") << ","
|
||
<< "\"mapping_deleted\":" << (mapping_deleted ? "true" : "false") << ","
|
||
<< "\"records_deleted\":" << records_deleted << ","
|
||
<< "\"k210_pending\":true}";
|
||
response_body = result.str();
|
||
|
||
std::cout << "[DELETE_FACE] Result: " << response_body << std::endl;
|
||
|
||
} else if (method == "GET" && endpoint == "/api/pending_delete") {
|
||
// K210查询待删除列表
|
||
response_body = getPendingDeleteList();
|
||
std::cout << "[PENDING_DELETE] List fetched" << std::endl;
|
||
|
||
} else if (method == "POST" && endpoint == "/api/confirm_delete") {
|
||
// K210确认删除完成,从待删除列表移除
|
||
// 请求体格式: {"face_id":"xxx"}
|
||
std::string face_id;
|
||
size_t pos = request_body.find("\"face_id\":\"");
|
||
if (pos != std::string::npos) {
|
||
size_t start = pos + 11;
|
||
size_t end = request_body.find("\"", start);
|
||
if (end != std::string::npos) {
|
||
face_id = request_body.substr(start, end - start);
|
||
}
|
||
}
|
||
|
||
if (!face_id.empty() && removeFromPendingDelete(face_id)) {
|
||
response_body = "{\"success\":true,\"message\":\"Confirmed delete: " + face_id + "\"}";
|
||
std::cout << "[PENDING_DELETE] K210 confirmed delete: " << face_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/sync_request") {
|
||
// K210查询是否需要同步
|
||
response_body = getSyncRequest();
|
||
std::cout << "[SYNC] K210 checking sync request: " << response_body << std::endl;
|
||
|
||
} else if (method == "POST" && endpoint == "/api/sync_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/trigger_sync") {
|
||
// 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";
|
||
}
|
||
|
||
// 构建HTTP响应
|
||
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: *\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);
|
||
}
|
||
|
||
// ============= 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;
|
||
server_addr.sin_addr.s_addr = INADDR_ANY;
|
||
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 port " << 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: " << DB_FILE << std::endl;
|
||
std::cout << "----------------------------------------" << std::endl;
|
||
|
||
// 确保CSV文件存在
|
||
std::ifstream test(DB_FILE);
|
||
if (!test.good()) {
|
||
std::ofstream create(DB_FILE);
|
||
create << "Timestamp,Student_ID,Name,Confidence,Status,Class_ID\n";
|
||
create.close();
|
||
}
|
||
test.close();
|
||
|
||
// 启动HTTP服务器
|
||
std::thread http_thread(httpServerThread);
|
||
http_thread.join();
|
||
|
||
#ifdef _WIN32
|
||
WSACleanup();
|
||
#endif
|
||
|
||
return 0;
|
||
}
|