696 lines
22 KiB
C
696 lines
22 KiB
C
#include "Attendance.h"
|
||
#include "Delay.h"
|
||
#include "Debug.h"
|
||
#include "esp8266.h" // ✨ 添加ESP8266头文件
|
||
#include <string.h>
|
||
#include <stdlib.h> // ← 添加atoi函数声明
|
||
|
||
// 配置参数
|
||
#define MAX_ATTENDANCE_RECORDS 100 // 最大记录数量
|
||
#define MIN_CONFIDENCE_THRESHOLD 0.75 // 最小置信度阈值
|
||
#define MAX_TIME_SLOT_COUNT 10 // 最大时间段数量
|
||
#define MAX_CACHED_IDS 50 // 最大缓存学号数量(从服务器下载)
|
||
|
||
// 时间段配置(动态从服务器更新)
|
||
static TimeSlot time_slots[MAX_TIME_SLOT_COUNT];
|
||
static uint8_t time_slot_count = 0; // 当前有效时间段数量
|
||
|
||
// 今日已签到学号缓存(从服务器下载)
|
||
static char cached_student_ids[MAX_CACHED_IDS][20];
|
||
static uint8_t cached_id_count = 0;
|
||
|
||
// 默认时间段配置(在服务器配置下载失败时使用)
|
||
static const TimeSlot default_time_slots[] = {
|
||
{1, 8, 0, 9, 30, "Morning Check-in"}, // 上午签到 08:00-09:30
|
||
{1, 12, 0, 13, 0, "Noon Check-out"}, // 中午签退 12:00-13:00
|
||
};
|
||
#define DEFAULT_TIME_SLOT_COUNT 2
|
||
|
||
// 当前时间(由外部更新,如果有RTC则从RTC读取)
|
||
static uint8_t current_hour = 8;
|
||
static uint8_t current_minute = 30;
|
||
|
||
// 内部存储
|
||
static AttendanceRecord attendance_records[MAX_ATTENDANCE_RECORDS];
|
||
static uint16_t record_count = 0;
|
||
static uint32_t total_attendance_count = 0; // 总考勤人次
|
||
|
||
/**
|
||
* @brief 初始化考勤模块
|
||
*/
|
||
void Attendance_Init(void)
|
||
{
|
||
record_count = 0;
|
||
total_attendance_count = 0;
|
||
current_hour = 8;
|
||
current_minute = 0;
|
||
memset(attendance_records, 0, sizeof(attendance_records));
|
||
|
||
// 初始化为默认时间段配置
|
||
memcpy(time_slots, default_time_slots, sizeof(default_time_slots));
|
||
time_slot_count = DEFAULT_TIME_SLOT_COUNT;
|
||
|
||
// 清空缓存
|
||
cached_id_count = 0;
|
||
memset(cached_student_ids, 0, sizeof(cached_student_ids));
|
||
|
||
DEBUG_INFO("[Attendance] Initialized with time slot mode");
|
||
}
|
||
|
||
/**
|
||
* @brief 清空今日签到缓存
|
||
*/
|
||
void Attendance_ClearCache(void)
|
||
{
|
||
cached_id_count = 0;
|
||
memset(cached_student_ids, 0, sizeof(cached_student_ids));
|
||
DEBUG_INFO("[Attendance] Cache cleared");
|
||
}
|
||
|
||
/**
|
||
* @brief 添加学号到缓存
|
||
*/
|
||
void Attendance_AddToCache(const char* student_id)
|
||
{
|
||
if (cached_id_count >= MAX_CACHED_IDS) {
|
||
DEBUG_ERROR("[Attendance] Cache full, cannot add more IDs");
|
||
return;
|
||
}
|
||
|
||
// 检查是否已存在
|
||
for (uint8_t i = 0; i < cached_id_count; i++) {
|
||
if (strcmp(cached_student_ids[i], student_id) == 0) {
|
||
return; // 已存在,不重复添加
|
||
}
|
||
}
|
||
|
||
strncpy(cached_student_ids[cached_id_count], student_id, 19);
|
||
cached_student_ids[cached_id_count][19] = '\0';
|
||
cached_id_count++;
|
||
|
||
DEBUG_PRINT("[Attendance] Added to cache: %s (total: %d)\r\n", student_id, cached_id_count);
|
||
}
|
||
|
||
/**
|
||
* @brief 在本地缓存中查重
|
||
* @retval 1=在缓存中找到(重复), 0=未找到
|
||
*/
|
||
uint8_t Attendance_IsDuplicateInCache(const char* student_id)
|
||
{
|
||
for (uint8_t i = 0; i < cached_id_count; i++) {
|
||
if (strcmp(cached_student_ids[i], student_id) == 0) {
|
||
DEBUG_PRINT("[Attendance] Found in cache: %s\r\n", student_id);
|
||
return 1;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* @brief 从服务器加载今日已签到记录到缓存
|
||
* @retval 加载的记录数量
|
||
*/
|
||
uint8_t Attendance_LoadTodayRecordsFromServer(void)
|
||
{
|
||
char slot_start[8];
|
||
char slot_end[8];
|
||
char records_buffer[512];
|
||
|
||
// 获取当前时段时间
|
||
Attendance_GetCurrentSlotTime(slot_start, slot_end);
|
||
|
||
DEBUG_PRINT("[Attendance] Loading today's records for slot %s-%s\r\n", slot_start, slot_end);
|
||
|
||
// 清空旧缓存
|
||
Attendance_ClearCache();
|
||
|
||
// 从服务器下载
|
||
uint8_t count = ESP8266_DownloadTodayRecords(slot_start, slot_end, records_buffer, sizeof(records_buffer));
|
||
|
||
if (count == 0) {
|
||
DEBUG_INFO("[Attendance] No records to load or download failed");
|
||
return 0;
|
||
}
|
||
|
||
// 解析学号列表 (格式: "1","2","3")
|
||
char* ptr = records_buffer;
|
||
char student_id[20];
|
||
uint8_t loaded = 0;
|
||
|
||
while (*ptr != '\0' && loaded < MAX_CACHED_IDS) {
|
||
// 跳过引号和逗号
|
||
while (*ptr == '"' || *ptr == ',' || *ptr == ' ') ptr++;
|
||
|
||
if (*ptr == '\0') break;
|
||
|
||
// 提取学号
|
||
char* id_start = ptr;
|
||
while (*ptr != '"' && *ptr != ',' && *ptr != '\0') ptr++;
|
||
|
||
uint8_t len = ptr - id_start;
|
||
if (len > 0 && len < 20) {
|
||
strncpy(student_id, id_start, len);
|
||
student_id[len] = '\0';
|
||
Attendance_AddToCache(student_id);
|
||
loaded++;
|
||
}
|
||
}
|
||
|
||
DEBUG_PRINT("[Attendance] Loaded %d records from server\r\n", loaded);
|
||
return loaded;
|
||
}
|
||
|
||
/**
|
||
* @brief 更新时间段配置(从服务器下载后调用)
|
||
* @param slots 新的时间段配置数组
|
||
* @param count 时间段数量
|
||
*/
|
||
void Attendance_UpdateTimeSlots(TimeSlot* slots, uint8_t count)
|
||
{
|
||
if (count > MAX_TIME_SLOT_COUNT) {
|
||
DEBUG_PRINT("[Attendance] Too many time slots: %d (max: %d)\r\n", count, MAX_TIME_SLOT_COUNT);
|
||
return;
|
||
}
|
||
|
||
// 更新时间段配置
|
||
memcpy(time_slots, slots, count * sizeof(TimeSlot));
|
||
time_slot_count = count;
|
||
|
||
DEBUG_PRINT("[Attendance] Time slots updated: %d slots loaded\r\n", count);
|
||
|
||
// 调试:打印所有时间段
|
||
for (uint8_t i = 0; i < time_slot_count; i++) {
|
||
DEBUG_PRINT("[Attendance] Slot %d: %s (%02d:%02d-%02d:%02d) %s\r\n",
|
||
i, time_slots[i].name,
|
||
time_slots[i].start_hour, time_slots[i].start_minute,
|
||
time_slots[i].end_hour, time_slots[i].end_minute,
|
||
time_slots[i].enabled ? "ENABLED" : "DISABLED");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 从JSON字符串解析时间段配置
|
||
* @param json_str JSON字符串(格式:{"time_slots":[{"enabled":true,"start_time":"08:00",...}]})
|
||
* @retval 成功解析的时间段数量
|
||
*/
|
||
uint8_t Attendance_ParseTimeSlotsJSON(const char* json_str)
|
||
{
|
||
TimeSlot temp_slots[MAX_TIME_SLOT_COUNT];
|
||
uint8_t slot_count = 0;
|
||
|
||
DEBUG_INFO("[Attendance] Parsing time slots JSON...");
|
||
DEBUG_PRINT("[Attendance] JSON: %s\r\n", json_str);
|
||
|
||
// 查找 "time_slots" 数组
|
||
const char* ptr = strstr(json_str, "\"time_slots\"");
|
||
if (ptr == NULL) {
|
||
DEBUG_ERROR("[Attendance] Invalid JSON: no 'time_slots' field found");
|
||
return 0;
|
||
}
|
||
|
||
// 查找数组开始 '['
|
||
ptr = strchr(ptr, '[');
|
||
if (ptr == NULL) {
|
||
DEBUG_ERROR("[Attendance] Invalid JSON: no '[' found");
|
||
return 0;
|
||
}
|
||
ptr++; // 跳过 '['
|
||
|
||
// 解析每个时间段对象
|
||
while (*ptr != '\0' && *ptr != ']' && slot_count < MAX_TIME_SLOT_COUNT)
|
||
{
|
||
// 跳过空格、换行、逗号
|
||
while (*ptr == ' ' || *ptr == ',' || *ptr == '\n' || *ptr == '\r' || *ptr == '\t') ptr++;
|
||
|
||
// 查找对象开始
|
||
if (*ptr != '{') break;
|
||
ptr++; // 跳过 '{'
|
||
|
||
// 初始化当前时间段
|
||
temp_slots[slot_count].enabled = 1;
|
||
temp_slots[slot_count].start_hour = 0;
|
||
temp_slots[slot_count].start_minute = 0;
|
||
temp_slots[slot_count].end_hour = 0;
|
||
temp_slots[slot_count].end_minute = 0;
|
||
strcpy(temp_slots[slot_count].name, "Unknown");
|
||
|
||
// 解析对象字段
|
||
while (*ptr != '\0' && *ptr != '}')
|
||
{
|
||
// 跳过空格、换行、逗号
|
||
while (*ptr == ' ' || *ptr == ',' || *ptr == '\n' || *ptr == '\r' || *ptr == '\t') ptr++;
|
||
|
||
// 跳过引号
|
||
if (*ptr == '"') ptr++;
|
||
|
||
// 解析字段名
|
||
if (strncmp(ptr, "enabled", 7) == 0) {
|
||
ptr = strchr(ptr, ':');
|
||
if (ptr) {
|
||
ptr++;
|
||
while (*ptr == ' ') ptr++;
|
||
// 处理 true/false 或 1/0
|
||
temp_slots[slot_count].enabled = (*ptr == 't' || *ptr == '1') ? 1 : 0;
|
||
}
|
||
}
|
||
else if (strncmp(ptr, "start_time", 10) == 0) {
|
||
// 格式: "start_time": "08:30"
|
||
ptr = strchr(ptr, ':');
|
||
if (ptr) {
|
||
ptr++;
|
||
while (*ptr == ' ' || *ptr == '"') ptr++;
|
||
// 解析 "HH:MM"
|
||
temp_slots[slot_count].start_hour = (ptr[0] - '0') * 10 + (ptr[1] - '0');
|
||
temp_slots[slot_count].start_minute = (ptr[3] - '0') * 10 + (ptr[4] - '0');
|
||
}
|
||
}
|
||
else if (strncmp(ptr, "end_time", 8) == 0) {
|
||
// 格式: "end_time": "09:30"
|
||
ptr = strchr(ptr, ':');
|
||
if (ptr) {
|
||
ptr++;
|
||
while (*ptr == ' ' || *ptr == '"') ptr++;
|
||
// 解析 "HH:MM"
|
||
temp_slots[slot_count].end_hour = (ptr[0] - '0') * 10 + (ptr[1] - '0');
|
||
temp_slots[slot_count].end_minute = (ptr[3] - '0') * 10 + (ptr[4] - '0');
|
||
}
|
||
}
|
||
else if (strncmp(ptr, "name", 4) == 0) {
|
||
ptr = strchr(ptr, ':');
|
||
if (ptr) {
|
||
ptr++;
|
||
while (*ptr == ' ' || *ptr == '"') ptr++;
|
||
// 提取名称(直到遇到引号)
|
||
const char* name_start = ptr;
|
||
while (*ptr != '"' && *ptr != '\0') ptr++;
|
||
int len = ptr - name_start;
|
||
if (len > 19) len = 19;
|
||
strncpy(temp_slots[slot_count].name, name_start, len);
|
||
temp_slots[slot_count].name[len] = '\0';
|
||
}
|
||
}
|
||
|
||
// 移动到下一个字段或对象结束
|
||
while (*ptr != '\0' && *ptr != ',' && *ptr != '}') ptr++;
|
||
if (*ptr == ',') ptr++;
|
||
}
|
||
|
||
if (*ptr == '}') ptr++; // 跳过 '}'
|
||
|
||
DEBUG_PRINT("[Attendance] Parsed slot %d: %s (%02d:%02d-%02d:%02d) %s\r\n",
|
||
slot_count, temp_slots[slot_count].name,
|
||
temp_slots[slot_count].start_hour, temp_slots[slot_count].start_minute,
|
||
temp_slots[slot_count].end_hour, temp_slots[slot_count].end_minute,
|
||
temp_slots[slot_count].enabled ? "ENABLED" : "DISABLED");
|
||
|
||
slot_count++;
|
||
}
|
||
|
||
DEBUG_PRINT("[Attendance] Parsed %d time slots\r\n", slot_count);
|
||
|
||
// 应用解析结果
|
||
if (slot_count > 0) {
|
||
Attendance_UpdateTimeSlots(temp_slots, slot_count);
|
||
return slot_count;
|
||
}
|
||
|
||
DEBUG_ERROR("[Attendance] Failed to parse any time slots");
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* @brief 设置当前时间(由外部定期调用更新)
|
||
* @param hour 小时 (0-23)
|
||
* @param minute 分钟 (0-59)
|
||
*/
|
||
void Attendance_SetCurrentTime(uint8_t hour, uint8_t minute)
|
||
{
|
||
current_hour = hour;
|
||
current_minute = minute;
|
||
}
|
||
|
||
/**
|
||
* @brief 获取当前所处的考勤时段
|
||
* @retval 时段编号 (0-N),如果不在任何时段内则返回 -1
|
||
*/
|
||
int8_t Attendance_GetCurrentTimeSlot(void)
|
||
{
|
||
for (uint8_t i = 0; i < time_slot_count; i++)
|
||
{
|
||
// 跳过禁用的时间段
|
||
if (!time_slots[i].enabled) {
|
||
continue;
|
||
}
|
||
|
||
uint16_t current_minutes = current_hour * 60 + current_minute;
|
||
uint16_t start_minutes = time_slots[i].start_hour * 60 + time_slots[i].start_minute;
|
||
uint16_t end_minutes = time_slots[i].end_hour * 60 + time_slots[i].end_minute;
|
||
|
||
if (current_minutes >= start_minutes && current_minutes <= end_minutes)
|
||
{
|
||
return i; // 返回时段编号
|
||
}
|
||
}
|
||
|
||
return -1; // 不在任何考勤时段内
|
||
}
|
||
|
||
/**
|
||
* @brief 获取当前时段的开始和结束时间
|
||
* @param start_time 输出:开始时间(格式"HH:MM")
|
||
* @param end_time 输出:结束时间(格式"HH:MM")
|
||
*/
|
||
void Attendance_GetCurrentSlotTime(char* start_time, char* end_time)
|
||
{
|
||
int8_t current_slot = Attendance_GetCurrentTimeSlot();
|
||
|
||
if (current_slot >= 0 && current_slot < time_slot_count) {
|
||
sprintf(start_time, "%02d:%02d",
|
||
time_slots[current_slot].start_hour,
|
||
time_slots[current_slot].start_minute);
|
||
sprintf(end_time, "%02d:%02d",
|
||
time_slots[current_slot].end_hour,
|
||
time_slots[current_slot].end_minute);
|
||
} else {
|
||
// 不在任何时段,返回全天
|
||
strcpy(start_time, "00:00");
|
||
strcpy(end_time, "23:59");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 检查学生是否在服务器端已签到(服务器端查重)
|
||
* @param student_id 学生学号
|
||
* @retval 1=服务器显示已签到(重复), 0=未签到或查询失败
|
||
*/
|
||
uint8_t Attendance_IsDuplicateOnServer(const char* student_id)
|
||
{
|
||
char slot_start[8];
|
||
char slot_end[8];
|
||
|
||
// 获取当前时段时间
|
||
Attendance_GetCurrentSlotTime(slot_start, slot_end);
|
||
|
||
DEBUG_PRINT("[Attendance] Checking server for duplicate: %s (%s-%s)\r\n",
|
||
student_id, slot_start, slot_end);
|
||
|
||
// 调用ESP8266查询服务器(这个过程可能耗时,蜂鸣器状态由调用者控制)
|
||
return ESP8266_CheckDuplicate(student_id, slot_start, slot_end);
|
||
}
|
||
|
||
/**
|
||
* @brief 检查是否在当前时段重复打卡(本地+服务器)
|
||
* @param student_id 学生学号
|
||
* @retval 1=重复, 0=不重复
|
||
*/
|
||
uint8_t Attendance_IsDuplicate(const char* student_id)
|
||
{
|
||
int8_t current_slot = Attendance_GetCurrentTimeSlot();
|
||
|
||
if (current_slot < 0) {
|
||
return 0; // 不在考勤时段,不算重复
|
||
}
|
||
|
||
// 遍历现有记录,检查是否在当前时段已打卡
|
||
for (uint16_t i = 0; i < record_count; i++)
|
||
{
|
||
// 如果找到相同学号且在相同时段
|
||
if (strcmp(attendance_records[i].student_id, student_id) == 0 &&
|
||
attendance_records[i].time_slot == current_slot)
|
||
{
|
||
return 1; // 本时段已打卡,重复
|
||
}
|
||
}
|
||
|
||
return 0; // 不重复
|
||
}
|
||
|
||
/**
|
||
* @brief 处理人脸识别结果并记录考勤
|
||
* @param result 人脸识别结果
|
||
* @retval 考勤状态
|
||
*/
|
||
AttendanceStatus Attendance_ProcessFaceResult(FaceRecognitionResult* result)
|
||
{
|
||
// 1. 检查识别是否成功
|
||
if (result->success == 0)
|
||
{
|
||
return ATTENDANCE_UNKNOWN;
|
||
}
|
||
|
||
// 2. 检查置信度
|
||
if (result->confidence < MIN_CONFIDENCE_THRESHOLD)
|
||
{
|
||
return ATTENDANCE_LOW_CONFIDENCE;
|
||
}
|
||
|
||
// 3. 检查是否在考勤时段内
|
||
int8_t current_slot = Attendance_GetCurrentTimeSlot();
|
||
if (current_slot < 0)
|
||
{
|
||
DEBUG_PRINT("[Attendance] Not in time slot (Current: %02d:%02d)\r\n",
|
||
current_hour, current_minute);
|
||
return ATTENDANCE_OUT_OF_TIME;
|
||
}
|
||
|
||
DEBUG_PRINT("[Attendance] Current slot: %s (%02d:%02d)\r\n",
|
||
time_slots[current_slot].name, current_hour, current_minute);
|
||
|
||
// 4. 检查是否在当前时段重复打卡(本地记录检查)
|
||
if (Attendance_IsDuplicate(result->student_id))
|
||
{
|
||
DEBUG_PRINT("[Attendance] Local duplicate in slot: %s\r\n", time_slots[current_slot].name);
|
||
return ATTENDANCE_DUPLICATE;
|
||
}
|
||
|
||
// 5. 检查缓存中是否已签到(从服务器下载的今日记录)
|
||
if (Attendance_IsDuplicateInCache(result->student_id))
|
||
{
|
||
DEBUG_PRINT("[Attendance] Cache duplicate in slot: %s\r\n", time_slots[current_slot].name);
|
||
return ATTENDANCE_DUPLICATE;
|
||
}
|
||
|
||
// 6. 添加新记录
|
||
if (record_count < MAX_ATTENDANCE_RECORDS)
|
||
{
|
||
// 复制学号和姓名
|
||
strncpy(attendance_records[record_count].student_id, result->student_id, 19);
|
||
attendance_records[record_count].student_id[19] = '\0';
|
||
|
||
strncpy(attendance_records[record_count].student_name, result->student_name, 31);
|
||
attendance_records[record_count].student_name[31] = '\0';
|
||
|
||
// 记录置信度
|
||
attendance_records[record_count].confidence = result->confidence;
|
||
|
||
// 记录时间戳和时段
|
||
// 简化:使用系统运行时间戳(服务器会用接收时间替换)
|
||
attendance_records[record_count].timestamp = Get_Time_ms() / 1000;
|
||
attendance_records[record_count].time_slot = current_slot;
|
||
|
||
// 初始化同步状态
|
||
attendance_records[record_count].sync_status = SYNC_PENDING;
|
||
attendance_records[record_count].retry_count = 0;
|
||
attendance_records[record_count].last_retry_time = 0;
|
||
|
||
record_count++;
|
||
total_attendance_count++;
|
||
|
||
// 添加到缓存,防止重复签到
|
||
Attendance_AddToCache(result->student_id);
|
||
|
||
DEBUG_PRINT("[Attendance] Success! Slot=%s, Total=%d, Status=PENDING\r\n",
|
||
time_slots[current_slot].name, total_attendance_count);
|
||
|
||
return ATTENDANCE_SUCCESS;
|
||
}
|
||
else
|
||
{
|
||
// 记录已满,覆盖最早的记录
|
||
memmove(&attendance_records[0], &attendance_records[1],
|
||
sizeof(AttendanceRecord) * (MAX_ATTENDANCE_RECORDS - 1));
|
||
record_count = MAX_ATTENDANCE_RECORDS - 1;
|
||
|
||
// 在最后位置添加新记录
|
||
strncpy(attendance_records[record_count].student_id, result->student_id, 19);
|
||
attendance_records[record_count].student_id[19] = '\0';
|
||
strncpy(attendance_records[record_count].student_name, result->student_name, 31);
|
||
attendance_records[record_count].student_name[31] = '\0';
|
||
attendance_records[record_count].confidence = result->confidence;
|
||
|
||
// 简化:使用系统运行时间戳(服务器会用接收时间替换)
|
||
attendance_records[record_count].timestamp = Get_Time_ms() / 1000;
|
||
attendance_records[record_count].time_slot = current_slot;
|
||
attendance_records[record_count].sync_status = SYNC_PENDING;
|
||
attendance_records[record_count].retry_count = 0;
|
||
attendance_records[record_count].last_retry_time = 0;
|
||
|
||
record_count++;
|
||
total_attendance_count++;
|
||
|
||
return ATTENDANCE_SUCCESS;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 清空所有考勤记录
|
||
*/
|
||
void Attendance_ClearRecords(void)
|
||
{
|
||
record_count = 0;
|
||
memset(attendance_records, 0, sizeof(attendance_records));
|
||
// 注意:不清空 total_attendance_count,保留历史统计
|
||
}
|
||
|
||
/**
|
||
* @brief 获取累计考勤总数
|
||
*/
|
||
uint16_t Attendance_GetTotalCount(void)
|
||
{
|
||
return total_attendance_count;
|
||
}
|
||
|
||
/**
|
||
* @brief 获取当前缓存中的记录数
|
||
*/
|
||
uint16_t Attendance_GetActiveCount(void)
|
||
{
|
||
return record_count;
|
||
}
|
||
|
||
/**
|
||
* @brief 获取待同步记录的数量(PENDING或FAILED状态)
|
||
* @retval 待同步记录数
|
||
*/
|
||
uint16_t Attendance_GetPendingCount(void)
|
||
{
|
||
uint16_t count = 0;
|
||
for (uint16_t i = 0; i < record_count; i++)
|
||
{
|
||
if (attendance_records[i].sync_status == SYNC_PENDING ||
|
||
attendance_records[i].sync_status == SYNC_FAILED)
|
||
{
|
||
count++;
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
/**
|
||
* @brief 获取下一个待同步的记录(PENDING或FAILED状态)
|
||
* @param index 输出参数,返回记录的索引
|
||
* @retval 记录指针,如果没有待同步记录则返回NULL
|
||
*/
|
||
AttendanceRecord* Attendance_GetNextPendingRecord(uint16_t* index)
|
||
{
|
||
uint32_t current_time = Get_Time_ms();
|
||
|
||
// 优先处理PENDING状态的记录
|
||
for (uint16_t i = 0; i < record_count; i++)
|
||
{
|
||
if (attendance_records[i].sync_status == SYNC_PENDING)
|
||
{
|
||
if (index != NULL)
|
||
{
|
||
*index = i;
|
||
}
|
||
return &attendance_records[i];
|
||
}
|
||
}
|
||
|
||
// 然后处理FAILED状态的记录(需要等待重试间隔)
|
||
for (uint16_t i = 0; i < record_count; i++)
|
||
{
|
||
if (attendance_records[i].sync_status == SYNC_FAILED)
|
||
{
|
||
// 指数退避算法:重试间隔 = 2^retry_count * 1000ms (最大30秒)
|
||
uint32_t retry_interval = (1 << attendance_records[i].retry_count) * 1000;
|
||
if (retry_interval > 30000) retry_interval = 30000;
|
||
|
||
// 检查是否到达重试时间
|
||
if (current_time - attendance_records[i].last_retry_time >= retry_interval)
|
||
{
|
||
if (index != NULL)
|
||
{
|
||
*index = i;
|
||
}
|
||
return &attendance_records[i];
|
||
}
|
||
}
|
||
}
|
||
|
||
return NULL; // 没有待同步记录
|
||
}
|
||
|
||
/**
|
||
* @brief 更新记录的同步状态
|
||
* @param index 记录索引
|
||
* @param status 新的同步状态
|
||
*/
|
||
void Attendance_MarkSyncStatus(uint16_t index, SyncStatus status)
|
||
{
|
||
if (index < record_count)
|
||
{
|
||
attendance_records[index].sync_status = status;
|
||
|
||
if (status == SYNC_FAILED || status == SYNC_SYNCING)
|
||
{
|
||
attendance_records[index].last_retry_time = Get_Time_ms();
|
||
}
|
||
|
||
const char* status_str[] = {"PENDING", "SYNCING", "SYNCED", "FAILED"};
|
||
DEBUG_PRINT("[Attendance] Record %d status: %s (ID=%s)\r\n",
|
||
index, status_str[status], attendance_records[index].student_id);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 增加记录的重试计数
|
||
* @param index 记录索引
|
||
*/
|
||
void Attendance_IncrementRetry(uint16_t index)
|
||
{
|
||
if (index < record_count)
|
||
{
|
||
attendance_records[index].retry_count++;
|
||
DEBUG_PRINT("[Attendance] Record %d retry count: %d\r\n",
|
||
index, attendance_records[index].retry_count);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 将所有FAILED记录重置为PENDING(用于网络恢复后)
|
||
*/
|
||
void Attendance_ResetFailedRecords(void)
|
||
{
|
||
uint16_t reset_count = 0;
|
||
for (uint16_t i = 0; i < record_count; i++)
|
||
{
|
||
if (attendance_records[i].sync_status == SYNC_FAILED)
|
||
{
|
||
attendance_records[i].sync_status = SYNC_PENDING;
|
||
attendance_records[i].retry_count = 0;
|
||
reset_count++;
|
||
}
|
||
}
|
||
|
||
if (reset_count > 0)
|
||
{
|
||
DEBUG_PRINT("[Attendance] Reset %d failed records to pending\r\n", reset_count);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 按索引获取记录
|
||
* @param index 记录索引
|
||
* @retval 记录指针,如果索引无效则返回NULL
|
||
*/
|
||
AttendanceRecord* Attendance_GetRecordByIndex(uint16_t index)
|
||
{
|
||
if (index < record_count)
|
||
{
|
||
return &attendance_records[index];
|
||
}
|
||
return NULL;
|
||
}
|