Files
2026-05-21 08:07:16 +08:00

782 lines
26 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Attendance.h"
#include "Delay.h"
#include "Debug.h"
#include "esp8266.h" // ✨ 添加ESP8266头文件
#include <string.h>
#include <stdlib.h> // ← 添加atoi函数声明
extern uint8_t g_server_connected;
// 配置参数
#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 int8_t cached_time_slots[MAX_CACHED_IDS];
static uint8_t cached_id_count = 0;
static int8_t active_cache_slot = -1;
// 当前时间(由外部更新,如果有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; // 总考勤人次
/* 函数作用:根据当前小时和分钟查找所处的考勤时间段。 */
static int8_t Attendance_FindTimeSlot(uint8_t hour, uint8_t minute)
{
uint16_t current_minutes = hour * 60 + minute;
for (uint8_t i = 0; i < time_slot_count; i++)
{
if (!time_slots[i].enabled) {
continue;
}
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 初始化考勤模块
*/
/* 函数作用:初始化考勤记录、时间段配置和今日已签到缓存。 */
void Attendance_Init(void)
{
record_count = 0;
total_attendance_count = 0;
current_hour = 8;
current_minute = 0;
memset(attendance_records, 0, sizeof(attendance_records));
// 启动时不预置考勤时段,等待服务端或上位机下发配置。
memset(time_slots, 0, sizeof(time_slots));
time_slot_count = 0;
// 清空缓存
cached_id_count = 0;
memset(cached_student_ids, 0, sizeof(cached_student_ids));
memset(cached_time_slots, 0xFF, sizeof(cached_time_slots));
active_cache_slot = -1;
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));
memset(cached_time_slots, 0xFF, sizeof(cached_time_slots));
active_cache_slot = Attendance_GetCurrentTimeSlot();
DEBUG_INFO("[Attendance] Cache cleared");
}
/**
* @brief 添加学号到缓存
*/
/* 函数作用:把指定学号加入当前考勤时段的本地查重缓存。 */
void Attendance_AddToCache(const char* student_id)
{
int8_t current_slot = Attendance_GetCurrentTimeSlot();
if (current_slot < 0) {
DEBUG_INFO("[Attendance] Not in any slot, skip cache add");
return;
}
if (cached_id_count >= MAX_CACHED_IDS) {
DEBUG_ERROR("[Attendance] Cache full, cannot add more IDs");
return;
}
if (active_cache_slot != current_slot) {
Attendance_ClearCache();
active_cache_slot = current_slot;
}
// 检查是否已存在
for (uint8_t i = 0; i < cached_id_count; i++) {
if (cached_time_slots[i] == current_slot && 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_time_slots[cached_id_count] = current_slot;
cached_id_count++;
DEBUG_PRINT("[Attendance] Added to cache: %s slot=%d (total: %d)\r\n",
student_id, current_slot, cached_id_count);
}
/**
* @brief 在本地缓存中查重
* @retval 1=在缓存中找到(重复), 0=未找到
*/
/* 函数作用:检查指定学号是否已存在于当前时段缓存中。 */
uint8_t Attendance_IsDuplicateInCache(const char* student_id)
{
int8_t current_slot = Attendance_GetCurrentTimeSlot();
if (current_slot < 0) {
return 0;
}
if (active_cache_slot != current_slot) {
Attendance_ClearCache();
active_cache_slot = current_slot;
return 0;
}
for (uint8_t i = 0; i < cached_id_count; i++) {
if (cached_time_slots[i] == current_slot && strcmp(cached_student_ids[i], student_id) == 0) {
DEBUG_PRINT("[Attendance] Found in cache: %s slot=%d\r\n", student_id, current_slot);
return 1;
}
}
return 0;
}
/**
* @brief 从服务器加载今日已签到记录到缓存
* @retval 加载的记录数量
*/
/* 函数作用:下载当前时段已签到学号列表并写入本地查重缓存。 */
uint8_t Attendance_LoadTodayRecordsFromServer(void)
{
char slot_start[8];
char slot_end[8];
char records_buffer[512];
int8_t current_slot = Attendance_GetCurrentTimeSlot();
if (current_slot < 0) {
Attendance_ClearCache();
DEBUG_INFO("[Attendance] Not in time slot, skip loading all-day cache");
return 0;
}
// 获取当前时段时间
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();
active_cache_slot = current_slot;
// 从服务器下载
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 成功解析的时间段数量
*/
/* 函数作用:从服务端同步 JSON 中解析时间段配置并应用到本地。 */
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 == ' ' || *ptr == '\n' || *ptr == '\r' || *ptr == '\t') ptr++;
if (*ptr == ']') {
Attendance_UpdateTimeSlots(temp_slots, 0);
DEBUG_INFO("[Attendance] Empty time slot config applied");
return 0;
}
// 解析每个时间段对象
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)
{
int8_t old_slot = Attendance_FindTimeSlot(current_hour, current_minute);
int8_t new_slot = Attendance_FindTimeSlot(hour, minute);
current_hour = hour;
current_minute = minute;
if (old_slot != new_slot) {
Attendance_ClearCache();
active_cache_slot = new_slot;
DEBUG_PRINT("[Attendance] Slot changed: %d -> %d, cache reset\r\n", old_slot, new_slot);
}
}
/**
* @brief 获取当前所处的考勤时段
* @retval 时段编号 (0-N),如果不在任何时段内则返回 -1
*/
/* 函数作用:返回当前时间对应的考勤时段索引。 */
int8_t Attendance_GetCurrentTimeSlot(void)
{
return Attendance_FindTimeSlot(current_hour, current_minute);
}
/**
* @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 (g_server_connected && Attendance_IsDuplicateOnServer(result->student_id))
{
DEBUG_PRINT("[Attendance] Server duplicate in slot: %s\r\n", time_slots[current_slot].name);
Attendance_AddToCache(result->student_id);
return ATTENDANCE_DUPLICATE;
}
// 7. 添加新记录
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;
}