diff --git a/Hardware/Attendance.c b/Hardware/Attendance.c index 271d490..7e1405f 100644 --- a/Hardware/Attendance.c +++ b/Hardware/Attendance.c @@ -23,15 +23,6 @@ static int8_t cached_time_slots[MAX_CACHED_IDS]; static uint8_t cached_id_count = 0; static int8_t active_cache_slot = -1; -// 默认时间段配置(在服务器配置下载失败时使用) -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 - {1, 14, 0, 15, 0, "Afternoon Check-in"}, // 下午签到 14:00-15:00 - {1, 17, 0, 18, 30, "Evening Check-out"}, // 晚上签退 17:00-18:30 -}; -#define DEFAULT_TIME_SLOT_COUNT 4 - // 当前时间(由外部更新,如果有RTC则从RTC读取) static uint8_t current_hour = 8; static uint8_t current_minute = 30; @@ -74,9 +65,9 @@ void Attendance_Init(void) 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; + // 启动时不预置考勤时段,等待服务端或上位机下发配置。 + memset(time_slots, 0, sizeof(time_slots)); + time_slot_count = 0; // 清空缓存 cached_id_count = 0; @@ -283,8 +274,9 @@ uint8_t Attendance_ParseTimeSlotsJSON(const char* json_str) while (*ptr == ' ' || *ptr == '\n' || *ptr == '\r' || *ptr == '\t') ptr++; if (*ptr == ']') { - DEBUG_INFO("[Attendance] Empty time slot config, keeping current slots"); - return time_slot_count; + Attendance_UpdateTimeSlots(temp_slots, 0); + DEBUG_INFO("[Attendance] Empty time slot config applied"); + return 0; } // 解析每个时间段对象 diff --git a/QT/README.md b/QT/README.md index 92d1a45..64434bf 100644 --- a/QT/README.md +++ b/QT/README.md @@ -1,517 +1,157 @@ -# 人脸识别考勤系统 +# Qt 管理端 README -基于 Qt 6 Quick/QML 开发的跨平台人脸识别考勤管理客户端,用于连接考勤服务端,完成签到记录查看、筛选导出、统计分析、签到时段配置、学生与班级名称映射、人脸照片与学号绑定等管理工作。 +这是人脸识别考勤系统的 Qt 管理端,支持 Windows 桌面端和 Android 端。更完整的说明见: -本项目是纪田野的个人毕业设计作品,主要用于人脸识别考勤场景的功能展示、系统联调和学习研究。 +```text +../docs/04_Qt管理端文档.md +../docs/00_项目总文档.md +``` -## 项目特点 +## 1. 功能概览 -- 跨平台客户端:支持 Windows 桌面端和 Android 端构建。 -- 单窗口管理:考勤记录、统计分析、配置中心、人脸管理和系统说明集中在一个界面中。 -- 响应式界面:针对手机窄屏做了独立布局适配,避免控件重叠和文字遮挡。 -- 数据导出:支持将考勤记录和统计报表导出为 CSV 文件。 -- 本地配置缓存:服务端地址、签到时段、映射配置会保存到本地。 -- 自动重连:服务端不可达时自动重连,并使用简短状态提示。 -- Android 字体优化:Android 端使用原生文字渲染,并降低粗体权重,减少中文粗体重影。 -- Android 路径兼容:Gradle 配置已允许中文路径构建,并启用 AndroidX。 +Qt 管理端用于连接考勤服务端,完成以下工作: -## 技术栈 +1. 查看考勤记录。 +2. 按班级、学号、姓名和日期筛选记录。 +3. 导出考勤记录 CSV。 +4. 查看学生维度的统计分析。 +5. 导出统计 CSV。 +6. 配置服务端地址。 +7. 新增、编辑、删除、清空签到时间段。 +8. 配置学号到姓名、班级编号到班级名称的显示映射。 +9. 查看 K210 上传的人脸照片。 +10. 为 K210 face_id 绑定真实学号。 +11. 删除人脸信息,并可选择删除关联考勤记录。 + +## 2. 当前重要行为 + +### 2.1 默认服务端地址 + +```text +http://127.0.0.1:8082 +``` + +启动后会优先读取本地保存的 `server/url`。 + +### 2.2 默认签到时间段 + +当前默认签到时间段为空: + +```json +{ + "time_slots": [] +} +``` + +第一次使用时,需要在“配置中心”里新增时间段并保存同步到服务端。旧版本本地保存的四个默认时间段会被自动迁移为空配置。 + +### 2.3 API Token + +Qt 每个请求都会带: + +```text +X-Api-Token: +``` + +默认 Token 写在 `src/ApiClient.cpp`。可以通过环境变量覆盖: + +```powershell +$env:ATTENDANCE_API_TOKEN="你的Token" +``` + +### 2.4 响应加密 + +服务端支持通过请求头 `X-Encrypted-Response: true` 返回 AES 加密响应,Qt 代码中保留了解密能力。 + +当前普通记录和统计请求没有默认开启这个请求头,实际按 JSON 明文解析。 + +## 3. 技术栈 | 分类 | 内容 | | --- | --- | -| 开发语言 | C++17、QML | -| UI 框架 | Qt Quick / Qt Quick Controls 2 | +| 语言 | C++17、QML | +| UI | Qt Quick / Qt Quick Controls 2 | | Qt 模块 | Core、Gui、Qml、Quick、QuickControls2、Network | -| 构建系统 | CMake 3.16+ | -| 桌面编译器 | MinGW / MSVC / Clang / GCC | -| Android 构建 | Qt for Android、Android SDK、Android NDK、JDK | -| 加密处理 | AES-128-CBC,代码位于 `src/CryptoUtils.cpp` | +| 构建 | CMake 3.16+ | +| 平台 | Windows 桌面端、Android | -## 目录结构 +## 4. 目录结构 ```text -rewrite_qt/ -├── android/ # Android 打包补充资源和 Gradle 配置 -│ ├── gradle.properties -│ └── res/mipmap-*/ # Android 启动图标 -├── assets/ # 桌面端图标资源 -├── cmake/ # 平台相关模板文件 -├── qml/ -│ └── Main.qml # 主界面 +QT/ +├── android/ # Android 打包配置和图标 +├── assets/ # 桌面端图标 +├── cmake/ # 平台模板 +├── qml/Main.qml # 主界面 ├── src/ -│ ├── ApiClient.* # 服务端 API 客户端 -│ ├── AppController.* # QML 与业务逻辑中枢 -│ ├── AttendanceTypes.h # 考勤数据结构 -│ ├── CryptoUtils.* # AES 解密工具 -│ ├── *ListModel.* # 暴露给 QML 的列表模型 -│ └── crypto/tiny_aes.hpp # AES 底层实现 +│ ├── ApiClient.* # HTTP API 客户端 +│ ├── AppController.* # 业务逻辑中枢 +│ ├── AttendanceTypes.h # 数据结构 +│ ├── CryptoUtils.* # AES 解密兼容工具 +│ └── *ListModel.* # QML 列表模型 ├── CMakeLists.txt ├── LICENSE ├── README.md └── main.cpp ``` -## 功能说明 +## 5. 构建环境 -### 考勤记录 +基础要求: -考勤记录页用于查看和管理服务端同步下来的签到记录。 +1. Qt 6.2 或更高。 +2. CMake 3.16 或更高。 +3. 支持 C++17 的编译器。 -- 查看总记录数、今日签到数、当前筛选结果数。 -- 按班级、学号、姓名关键字筛选记录。 -- 按全部时间、今天、最近 7 天、最近 30 天、指定日期筛选。 -- 刷新最新记录。 -- 导出当前筛选结果为 CSV。 -- 清空服务端全部考勤记录。 +Windows 推荐使用 Qt 6.x MinGW 64-bit Kit。 -导出字段: +Android 还需要: -```text -签到时间,班级编号,班级名称,学号,姓名,置信度,状态 -``` +1. Qt for Android。 +2. Android SDK。 +3. Android NDK。 +4. JDK。 +5. Android 平台版本,例如 `android-35`。 -### 统计分析 - -统计分析页基于当前加载的考勤记录在本地计算统计结果。 - -- 支持今天、最近 7 天、最近 30 天、本月等快捷范围。 -- 支持手动指定开始日期和结束日期。 -- 展示统计学生数、统计记录数、统计范围。 -- 按学生汇总总签到次数、今日签到次数、最后签到时间和平均置信度。 -- 导出统计报表为 CSV。 - -导出字段: - -```text -学号,姓名,总签到次数,今日签到,最后签到时间,平均置信度 -``` - -### 配置中心 - -配置中心用于维护客户端与服务端交互所需的配置。 - -- 设置服务端地址。 -- 测试服务端连接状态。 -- 管理签到时段。 -- 从服务端同步签到时段。 -- 管理学号到姓名的映射。 -- 管理班级编号到班级名称的映射。 -- 保存配置并同步到服务端。 - -默认签到时段: - -| 名称 | 开始时间 | 结束时间 | 默认状态 | -| --- | --- | --- | --- | -| 上午签到 | 08:00 | 09:30 | 启用 | -| 中午签退 | 12:00 | 13:00 | 启用 | -| 下午签到 | 14:00 | 15:00 | 启用 | -| 晚上签退 | 17:00 | 18:30 | 启用 | - -### 人脸管理 - -人脸管理页用于维护 K210 上传的人脸照片记录与真实学号之间的映射关系。 - -- 同步人脸照片 ID。 -- 同步服务端学生 ID。 -- 展示人脸照片或无照片状态。 -- 为人脸 ID 绑定真实学号。 -- 保存人脸 ID 与学号映射。 -- 删除指定人脸信息。 -- 可选择同时删除该学生相关考勤记录。 - -照片访问地址格式: - -```text -{serverUrl}/api/face_photo/{id}?ts={timestamp} -``` - -其中 `ts` 用于避免缓存导致照片不刷新。 - -## 服务端配置 - -默认服务端地址: - -```text -http://127.0.0.1:8082 -``` - -应用启动后会从本地配置读取已保存的服务端地址。用户可在“配置中心”中修改并保存。 - -本地保存键名: - -```text -server/url -``` - -## 服务端 API - -客户端会基于服务端地址拼接 `/api` 前缀访问接口。 - -| 方法 | 路径 | 用途 | -| --- | --- | --- | -| `GET` | `/api/records?limit={limit}&offset={offset}` | 获取考勤记录 | -| `GET` | `/api/statistics` | 获取总数、今日数和服务端时间 | -| `POST` | `/api/clear` | 清空全部记录 | -| `DELETE` | `/api/record?timestamp={timestamp}&student_id={studentId}` | 删除单条记录 | -| `GET` | `/api/timeslots` | 获取签到时段 | -| `POST` | `/api/timeslots` | 保存签到时段 | -| `GET` | `/api/mapping_config` | 获取学生和班级映射 | -| `POST` | `/api/mapping_config` | 保存学生和班级映射 | -| `GET` | `/api/face_id_mapping` | 获取人脸 ID 到学号映射 | -| `POST` | `/api/face_id_mapping` | 保存人脸 ID 到学号映射 | -| `GET` | `/api/face_photos` | 获取照片 ID 列表 | -| `GET` | `/api/all_student_ids` | 获取所有学生 ID | -| `GET` | `/api/face_photo/{id}` | 获取人脸照片 | -| `DELETE` | `/api/face/{studentId}` | 删除人脸信息 | -| `DELETE` | `/api/face/{studentId}?delete_records=true` | 删除人脸信息并删除相关考勤记录 | -| `GET` | `/api/health` | 健康检查 | - -### 考勤记录格式 - -`/api/records` 解密后应返回 JSON 数组。单条记录示例: - -```json -{ - "timestamp": "2026-04-24 08:10:23", - "student_id": "20240001", - "name": "张三", - "confidence": 0.9821, - "status": "success", - "class_id": "1" -} -``` - -### 统计数据格式 - -```json -{ - "total": 120, - "today": 8, - "server_time": "2026-04-24 10:30:00" -} -``` - -### 签到时段格式 - -```json -{ - "time_slots": [ - { - "name": "上午签到", - "start_time": "08:00", - "end_time": "09:30", - "enabled": true - } - ] -} -``` - -### 映射配置格式 - -```json -{ - "student_name_map": { - "20240001": "张三" - }, - "class_name_map": { - "1": "软件工程一班" - } -} -``` - -### 人脸映射格式 - -```json -{ - "mappings": { - "face_001": "20240001" - } -} -``` - -## 加密响应 - -客户端对 `/api/records` 和 `/api/statistics` 请求添加请求头: - -```text -X-Encrypted-Response: true -``` - -客户端期望服务端返回 Base64 编码的 AES-128-CBC 密文。 - -解密参数位于 `src/CryptoUtils.cpp`: - -| 参数 | 值 | -| --- | --- | -| 算法 | AES-128-CBC | -| Padding | PKCS#7 | -| Key | `2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c` | -| IV | `00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f` | - -## 本地配置文件 - -应用使用 `QSettings` 和 `QStandardPaths::AppDataLocation` 保存本地状态。 - -| 配置 | 保存位置 | -| --- | --- | -| 服务端地址 | `QSettings`,键名 `server/url` | -| 学生和班级映射 | `mapping_config.json` | -| 签到时段配置 | `timeslot_config.json` | - -应用信息: - -```text -ApplicationName: 人脸识别考勤系统 -ApplicationVersion: 3.0.0 -OrganizationName: STM32 Face Recognition -``` - -## 构建环境 - -### 基础要求 - -- Qt 6.2 或更高版本 -- CMake 3.16 或更高版本 -- 支持 C++17 的编译器 - -Windows 桌面端推荐使用 Qt 6.x MinGW 64-bit Kit。 - -Android 端需要额外配置: - -- Qt for Android -- Android SDK -- Android NDK -- JDK -- Android 平台版本,例如 `android-35` - -## 使用 Qt Creator 构建 +## 6. Qt Creator 构建 1. 打开 Qt Creator。 -2. 选择项目根目录下的 `CMakeLists.txt`。 -3. 选择对应 Kit,例如 Desktop Qt 6.x 或 Qt for Android。 +2. 打开 `QT/CMakeLists.txt`。 +3. 选择 Desktop Qt 或 Android Kit。 4. 等待 CMake 配置完成。 5. 构建目标 `apprewrite_qt`。 -6. 桌面端可直接运行;Android 端可部署到设备或模拟器。 +6. 桌面端直接运行;Android 端部署到设备或模拟器。 -## 命令行构建 +## 7. 命令行构建示例 Windows 示例: ```powershell -cmake -S . -B build -DCMAKE_PREFIX_PATH=D:/Qt/6.10.0/mingw_64 -cmake --build build --target apprewrite_qt +cd D:\University\graduation\code\Reconstruct_Qt\QT +cmake -S . -B build\Desktop -DCMAKE_PREFIX_PATH=D:\Qt\6.10.0\mingw_64 +cmake --build build\Desktop ``` -Linux 示例: +Qt 安装路径需要按本机实际情况修改。 -```bash -cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x/gcc_64 -cmake --build build --target apprewrite_qt -``` +## 8. 本地数据 -安装到系统路径: +Qt 会保存这些本地数据: -```bash -cmake --install build -``` - -## Windows 独立运行 - -直接双击构建出来的 `apprewrite_qt.exe` 时,如果提示找不到 `Qt6Gui.dll`、`Qt6Core.dll` 等动态库,需要使用 `windeployqt` 收集运行时依赖。 - -示例: - -```powershell -D:\Qt\6.10.0\mingw_64\bin\windeployqt.exe "D:\path\to\build\Desktop_Qt_6_10_0_MinGW_64_bit-Debug\apprewrite_qt.exe" -``` - -如果项目路径包含中文导致部署工具输出异常,建议复制 exe 到纯英文路径后部署: - -```powershell -mkdir C:\tmp\rewrite_qt_release -copy "D:\path\to\apprewrite_qt.exe" C:\tmp\rewrite_qt_release\ -D:\Qt\6.10.0\mingw_64\bin\windeployqt.exe C:\tmp\rewrite_qt_release\apprewrite_qt.exe -``` - -部署完成后,将整个发布目录一起复制到目标电脑运行。 - -## Android 构建说明 - -Android 相关配置位于 `CMakeLists.txt`: - -```cmake -QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android" -QT_ANDROID_APP_NAME "${REWRITE_QT_DISPLAY_NAME}" -QT_ANDROID_APP_ICON "@mipmap/ic_launcher" -``` - -Android 补充配置位于 `android/gradle.properties`: - -```properties -android.overridePathCheck=true -android.useAndroidX=true -``` - -说明: - -- `android.overridePathCheck=true` 用于绕过 Windows 下 Android Gradle 对中文路径的检查。 -- `android.useAndroidX=true` 用于启用 AndroidX,避免依赖检查失败。 -- Android 桌面显示名由 `QT_ANDROID_APP_NAME` 设置为“人脸识别考勤系统”。 -- Android 启动图标位于 `android/res/mipmap-*`。 - -如果 Android 构建目录已经生成过旧的 Gradle 工程,修改配置后建议清理对应构建目录或重新运行 CMake 配置。 - -## 图标资源 - -桌面端图标位于 `assets/`: - -```text -assets/favicon.ico -assets/favicon-16x16.png -assets/favicon-32x32.png -assets/favicon-48x48.png -assets/favicon-64x64.png -assets/favicon-128x128.png -assets/favicon-192x192.png -assets/favicon-256x256.png -``` - -Android 启动图标位于: - -```text -android/res/mipmap-mdpi/ic_launcher.png -android/res/mipmap-hdpi/ic_launcher.png -android/res/mipmap-xhdpi/ic_launcher.png -android/res/mipmap-xxhdpi/ic_launcher.png -android/res/mipmap-xxxhdpi/ic_launcher.png -``` - -图标尺寸: - -| 目录 | 尺寸 | +| 数据 | 保存方式 | | --- | --- | -| `mipmap-mdpi` | 48x48 | -| `mipmap-hdpi` | 72x72 | -| `mipmap-xhdpi` | 96x96 | -| `mipmap-xxhdpi` | 144x144 | -| `mipmap-xxxhdpi` | 192x192 | +| 服务端地址 | `QSettings`,键名 `server/url` | +| 时间段配置 | `QStandardPaths::AppDataLocation/timeslot_config.json` | +| 学生/班级映射 | `QStandardPaths::AppDataLocation/mapping_config.json` | +| 人脸照片缓存 | `QStandardPaths::AppDataLocation/face_photos/*.jpg` | -## 界面与移动端适配 +## 9. 常见问题 -主界面位于 `qml/Main.qml`,采用 `ApplicationWindow`、`StackLayout`、`GridLayout`、`ColumnLayout` 等组件组织页面。 +| 现象 | 处理 | +| --- | --- | +| 测试连接失败 | 检查服务端是否启动、地址是否正确、Token 是否一致 | +| 没有时间段 | 这是当前默认行为,需要手动新增并保存 | +| 识别成功但没有记录 | 检查 STM32 是否在时间段内、是否成功加密上传 | +| 人脸照片不显示 | 检查 K210 是否上传照片,服务端 `faces/` 是否有图片 | +| Android 布局拥挤 | 当前 QML 已做窄屏自适应,若仍异常请检查实际设备宽度 | -当前界面包含五个页面: - -- 考勤记录 -- 统计分析 -- 配置中心 -- 人脸管理 -- 关于 - -移动端适配要点: - -- `phoneLayout` 用于 560px 以下窄屏布局。 -- 顶部页签使用统一的 `MobileNavTabButton` 样式。 -- 考勤筛选区使用两列布局,按钮等宽排布。 -- 统计页前两张统计卡左右排布,统计范围独占一整行。 -- 表格在窄屏隐藏部分非关键列,避免横向溢出。 -- 空状态卡片会根据可用高度自动缩小。 -- Android 端使用 `sans-serif` 字体和 `Font.Medium` 加粗权重,减少粗体重影。 - -## 运行状态提示 - -客户端通过底部 Toast 展示状态信息。服务端不可达时,提示会被压缩为简短文案: - -```text -服务器未连接,稍后重试 -``` - -重连过程中显示: - -```text -正在重连服务器 -``` - -这样可以避免将底层网络错误、URL 和长异常文本直接展示给用户。 - -## 常见问题 - -### Android 构建提示项目路径包含中文 - -项目已经在 `android/gradle.properties` 中配置: - -```properties -android.overridePathCheck=true -``` - -如果仍然失败,清理 Android 构建目录后重新构建。 - -### Android 构建提示 AndroidX 未启用 - -项目已经在 `android/gradle.properties` 中配置: - -```properties -android.useAndroidX=true -``` - -如果旧构建目录仍然报错,删除生成的 `android-build-*` 目录后重新部署。 - -### Android 端粗体文字重影 - -项目在 Android 端启用了原生文字渲染: - -```cpp -QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); -``` - -同时 QML 中将 Android 端粗体权重降为 `Font.Medium`,避免合成粗体产生重影。 - -### Windows 双击 exe 提示缺少 Qt DLL - -使用 `windeployqt` 部署依赖后再运行。部署时建议使用和构建 Kit 一致的 Qt 目录。 - -### 修改图标后没有变化 - -重新运行 CMake 配置并重新构建。Windows 资源管理器可能缓存 exe 图标,可尝试换构建目录、改输出文件名或重启资源管理器。 - -### 启动后没有数据 - -检查服务端地址是否正确,并在配置中心点击“测试连接”。如果服务端不可达,底部会显示简短连接失败提示。 - -### 姓名或班级显示不正确 - -在配置中心维护学生姓名映射和班级名称映射,保存后记录列表和统计结果会重新计算。 - -### 人脸照片不显示 - -检查 `/api/face_photos` 是否返回照片 ID,并确认 `/api/face_photo/{id}` 可以访问。 - -## 仓库与作者 - -仓库地址: - -```text -ssh://git@git.yzfh.eu.cc:2084/yzfh/qt.git -``` - -作者: - -```text -纪田野 -``` - -关于页中也展示了仓库地址和查看账号信息。若项目公开发布,请避免在仓库中提交真实密码。 - -## 维护建议 - -- 不要提交 `build/`、`build-codex/` 等构建目录。 -- 修改 QML 后建议运行 `qmlformat` 保持格式一致。 -- 服务端记录量超过 1000 条时,需要扩展分页加载逻辑。 -- 服务端加密参数变化时,需要同步修改 `src/CryptoUtils.cpp`。 -- 发布桌面版本时,使用 Qt 部署工具收集运行时依赖。 -- 发布 Android 版本时,建议补充签名配置和 release 构建流程。 -- 如果需要修改历史提交作者,请在推送前完成,避免远程协作历史冲突。 - -## 开源协议 - -本项目采用 MIT License 开源,详见 `LICENSE` 文件。 diff --git a/QT/qml/Main.qml b/QT/qml/Main.qml index 4c84f0c..eec6dc6 100644 --- a/QT/qml/Main.qml +++ b/QT/qml/Main.qml @@ -7,8 +7,8 @@ ApplicationWindow { id: window width: 1560 height: 920 - minimumWidth: 900 - minimumHeight: 620 + minimumWidth: Qt.platform.os === "android" ? 360 : 900 + minimumHeight: Qt.platform.os === "android" ? 560 : 620 visible: true title: "人脸识别考勤系统" color: "#f6f7f9" @@ -41,11 +41,12 @@ ApplicationWindow { property int currentPageIndex: 0 property bool timeSlotsExpanded: false property bool mappingsExpanded: false + readonly property bool androidLayout: Qt.platform.os === "android" readonly property bool compactLayout: width < 1460 readonly property bool denseLayout: width < 1380 readonly property bool narrowLayout: width < 1280 readonly property bool slimLayout: width < 1040 - readonly property bool phoneLayout: width < 560 + readonly property bool phoneLayout: width < (androidLayout ? 640 : 560) readonly property int sidebarWidth: slimLayout ? 0 : (compactLayout ? 212 : 240) font.family: appFontFamily @@ -169,12 +170,14 @@ ApplicationWindow { } contentItem: Label { + width: control.availableWidth text: control.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter color: control.labelColor font.pixelSize: 13 font.weight: appBoldWeight + elide: Text.ElideRight } } @@ -194,12 +197,14 @@ ApplicationWindow { } contentItem: Label { + width: control.availableWidth text: control.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter color: control.labelColor font.pixelSize: 13 font.weight: appBoldWeight + elide: Text.ElideRight } } @@ -1972,7 +1977,7 @@ ApplicationWindow { radius: 8 color: panelStrong border.color: lineColor - Layout.preferredHeight: phoneLayout ? 218 : (slimLayout ? 174 : 88) + Layout.preferredHeight: serverConfigGrid.implicitHeight + (slimLayout ? 24 : 36) clip: true GridLayout { @@ -1998,13 +2003,13 @@ ApplicationWindow { } SecondaryButton { - Layout.fillWidth: phoneLayout + Layout.fillWidth: slimLayout text: "保存地址" onClicked: appController.serverUrl = serverUrlField.text } PrimaryButton { - Layout.fillWidth: phoneLayout + Layout.fillWidth: slimLayout text: "测试连接" fillColor: tealColor onClicked: appController.testConnection() @@ -2017,19 +2022,17 @@ ApplicationWindow { radius: 8 color: panelStrong border.color: lineColor - Layout.preferredHeight: timeSlotsExpanded - ? 18 + (phoneLayout ? 132 : 64) + 12 + (phoneLayout ? 0 : 42) + (phoneLayout ? 0 : 12) + Math.max(phoneLayout ? 96 : 52, timeSlotListView.contentHeight) + 18 - : (phoneLayout ? 124 : 100) + Layout.preferredHeight: timeSlotCardLayout.implicitHeight + 36 clip: true ColumnLayout { + id: timeSlotCardLayout anchors.fill: parent anchors.margins: 18 spacing: 12 ColumnLayout { Layout.fillWidth: true - Layout.preferredHeight: phoneLayout ? 132 : 64 spacing: 12 SectionHeader { @@ -2040,7 +2043,7 @@ ApplicationWindow { } GridLayout { Layout.fillWidth: true - columns: phoneLayout ? 3 : 5 + columns: slimLayout ? 3 : 5 columnSpacing: 8 rowSpacing: 8 @@ -2058,7 +2061,7 @@ ApplicationWindow { SecondaryButton { Layout.fillWidth: true visible: timeSlotsExpanded - text: "恢复默认" + text: "清空时段" onClicked: { appController.timeSlotModel.resetToDefault(); appController.saveTimeSlots(); @@ -2067,13 +2070,13 @@ ApplicationWindow { SecondaryButton { Layout.fillWidth: true visible: timeSlotsExpanded - text: phoneLayout ? "同步" : "从服务器同步" + text: slimLayout ? "同步" : "从服务器同步" onClicked: appController.reloadTimeSlotsFromServer() } PrimaryButton { Layout.fillWidth: true visible: timeSlotsExpanded - text: phoneLayout ? "保存" : "保存时间段" + text: slimLayout ? "保存" : "保存时间段" onClicked: appController.saveTimeSlots() } } @@ -2120,7 +2123,7 @@ ApplicationWindow { id: timeSlotListView visible: timeSlotsExpanded Layout.fillWidth: true - Layout.preferredHeight: visible ? contentHeight : 0 + Layout.preferredHeight: visible ? Math.max(phoneLayout ? 96 : 52, contentHeight) : 0 clip: false interactive: false spacing: 8 @@ -2253,19 +2256,17 @@ ApplicationWindow { radius: 8 color: panelStrong border.color: lineColor - Layout.preferredHeight: mappingsExpanded - ? 18 + (phoneLayout ? 120 : 64) + 12 + mappingTabs.implicitHeight + 12 + Math.max(phoneLayout ? 112 : 64, mappingTabs.currentIndex === 0 ? studentMappingListView.contentHeight : classMappingListView.contentHeight) + 18 - : (phoneLayout ? 124 : 100) + Layout.preferredHeight: mappingCardLayout.implicitHeight + 36 clip: true ColumnLayout { + id: mappingCardLayout anchors.fill: parent anchors.margins: 18 spacing: 12 ColumnLayout { Layout.fillWidth: true - Layout.preferredHeight: phoneLayout ? 120 : 64 spacing: 12 SectionHeader { Layout.fillWidth: true @@ -2324,7 +2325,7 @@ ApplicationWindow { StackLayout { visible: mappingsExpanded Layout.fillWidth: true - Layout.preferredHeight: visible ? (mappingTabs.currentIndex === 0 ? studentMappingListView.contentHeight : classMappingListView.contentHeight) : 0 + Layout.preferredHeight: visible ? Math.max(phoneLayout ? 112 : 64, mappingTabs.currentIndex === 0 ? studentMappingListView.contentHeight : classMappingListView.contentHeight) : 0 currentIndex: mappingTabs.currentIndex Rectangle { @@ -2861,7 +2862,7 @@ ApplicationWindow { TextArea { Layout.fillWidth: true Layout.fillHeight: true - text: "本系统为纪田野的个人毕业设计作品,仅用于毕业设计展示、学习研究与系统功能验证。\nCopyright © 2026 纪田野。保留所有权利。\n仓库地址:https://git.yzfh.eu.cc/yzfh/qt.git\n演示账号与密码请在实际部署环境中单独配置。" + text: "本系统为纪田野的个人毕业设计作品,仅用于毕业设计展示、学习研究与系统功能验证。\n\nCopyright © 2026 纪田野。保留所有权利。\n\n仓库地址:https://git.yzfh.eu.cc/yzfh/qt.git\n\n演示账号:jtybishe 密码:jtybishe" readOnly: true selectByMouse: true selectByKeyboard: true diff --git a/QT/src/AppController.cpp b/QT/src/AppController.cpp index a075f7d..62539e9 100644 --- a/QT/src/AppController.cpp +++ b/QT/src/AppController.cpp @@ -79,6 +79,35 @@ QString safeFileName(QString value) return value.isEmpty() ? QStringLiteral("unknown") : value; } +QVector legacyDefaultTimeSlots() +{ + return { + {"上午签到", QTime(8, 0), QTime(9, 30), true}, + {"中午签退", QTime(12, 0), QTime(13, 0), true}, + {"下午签到", QTime(14, 0), QTime(15, 0), true}, + {"晚上签退", QTime(17, 0), QTime(18, 30), true} + }; +} + +bool isLegacyDefaultTimeSlots(const QVector &timeSlots) +{ + const QVector legacy = legacyDefaultTimeSlots(); + if (timeSlots.size() != legacy.size()) { + return false; + } + + for (int i = 0; i < legacy.size(); ++i) { + if (timeSlots.at(i).name != legacy.at(i).name + || timeSlots.at(i).startTime != legacy.at(i).startTime + || timeSlots.at(i).endTime != legacy.at(i).endTime + || timeSlots.at(i).enabled != legacy.at(i).enabled) { + return false; + } + } + + return true; +} + } AppController::AppController(QObject *parent) @@ -131,11 +160,11 @@ AppController::AppController(QObject *parent) }); connect(&m_apiClient, &ApiClient::timeSlotsFetched, this, [this](const QVector &timeSlots) { - if (!timeSlots.isEmpty()) { - m_timeSlotModel.setTimeSlots(timeSlots); - saveTimeSlotsToLocal(); - setStatusMessage(QStringLiteral("已从服务器同步时间段")); - } + m_timeSlotModel.setTimeSlots(timeSlots); + saveTimeSlotsToLocal(); + setStatusMessage(timeSlots.isEmpty() + ? QStringLiteral("已从服务器同步空时间段配置") + : QStringLiteral("已从服务器同步时间段")); }); connect(&m_apiClient, &ApiClient::timeSlotsUploaded, this, [this](bool success, const QString &message) { @@ -425,8 +454,14 @@ void AppController::saveFaceMappings() QMap mapping; const QVector items = m_faceModel.items(); for (const FaceListModel::Item &item : items) { - if (!item.studentId.trimmed().isEmpty()) { - mapping[item.faceId] = item.studentId.trimmed(); + const QString faceId = item.faceId.trimmed(); + const QString studentId = item.studentId.trimmed(); + if (faceId.isEmpty() || studentId.isEmpty()) { + continue; + } + + if (m_faceIdMapping.contains(faceId) || faceId != studentId) { + mapping[faceId] = studentId; } } m_faceIdMapping = mapping; @@ -537,7 +572,7 @@ void AppController::loadTimeSlotsFromLocal() { QFile file(localSettingsPath("timeslot_config.json")); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - m_timeSlotModel.setTimeSlots(defaultTimeSlots()); + m_timeSlotModel.setTimeSlots({}); return; } @@ -553,7 +588,14 @@ void AppController::loadTimeSlotsFromLocal() }); } - m_timeSlotModel.setTimeSlots(timeSlots.isEmpty() ? defaultTimeSlots() : timeSlots); + if (isLegacyDefaultTimeSlots(timeSlots)) { + timeSlots.clear(); + } + + m_timeSlotModel.setTimeSlots(timeSlots); + if (timeSlots.isEmpty()) { + saveTimeSlotsToLocal(); + } } QString AppController::localSettingsPath(const QString &fileName) const @@ -725,6 +767,9 @@ void AppController::mergeFaceItems() for (const QString &id : m_allStudentIds) { allIds.insert(id); } + for (auto it = m_faceIdMapping.cbegin(); it != m_faceIdMapping.cend(); ++it) { + allIds.insert(it.key()); + } QStringList sortedIds = allIds.values(); std::sort(sortedIds.begin(), sortedIds.end()); diff --git a/QT/src/AttendanceTypes.h b/QT/src/AttendanceTypes.h index b58ffae..63d2c6f 100644 --- a/QT/src/AttendanceTypes.h +++ b/QT/src/AttendanceTypes.h @@ -42,12 +42,7 @@ struct TimeSlot inline QVector defaultTimeSlots() { - return { - {"上午签到", QTime(8, 0), QTime(9, 30), true}, - {"中午签退", QTime(12, 0), QTime(13, 0), true}, - {"下午签到", QTime(14, 0), QTime(15, 0), true}, - {"晚上签退", QTime(17, 0), QTime(18, 30), true} - }; + return {}; } Q_DECLARE_METATYPE(AttendanceRecord) diff --git a/User/main.c b/User/main.c index 248f651..2748323 100644 --- a/User/main.c +++ b/User/main.c @@ -423,7 +423,7 @@ uint8_t DownloadTimeSlots(void) // 解析JSON并更新时间段配置 uint8_t slot_count = Attendance_ParseTimeSlotsJSON(response_buffer); - if (slot_count > 0) { + if (slot_count > 0 || strstr(response_buffer, "\"time_slots\"") != NULL) { DEBUG_PRINT("Time slot configuration updated: %d slots\r\n", slot_count); if (server_version > 0) { g_stm32_sync_version = server_version; diff --git a/docs/00_项目总文档.md b/docs/00_项目总文档.md index c4bb92d..f40a3ab 100644 --- a/docs/00_项目总文档.md +++ b/docs/00_项目总文档.md @@ -1,49 +1,357 @@ -# 项目总文档 +# 人脸识别考勤系统项目总文档 -## 1. 项目概览 -本项目是“人脸识别考勤系统”多端协同工程,包含: -- STM32 固件端(设备控制与签到逻辑) -- K210 识别端(视觉识别与人脸管理) -- C++ 服务端(加密签到、数据管理、同步控制) -- Qt 管理端(后台管理与可视化) +## 1. 这是什么项目 -## 2. 推荐运行链路 -建议使用以下版本组合: -1. 服务端:`server/server_encrypted.cpp`(HTTP 8082) -2. STM32:`User/main.c`(加密上传 + 时间段同步) -3. K210:`k210/main.py`(识别 + 照片上传 + 同步) -4. Qt:`QT/CMakeLists.txt` 构建的客户端 +本项目是一个多端协同的人脸识别考勤系统。它不是只有一个程序,而是由四个主要部分组成: + +| 部分 | 所在目录 | 主要职责 | 可以把它理解成 | +| --- | --- | --- | --- | +| STM32 固件端 | `User/`、`Hardware/`、`System/`、`Library/`、`Start/` | 接收 K210 识别结果、判断是否允许签到、加密上传记录、控制 OLED/蜂鸣器/指示灯/补光灯 | 考勤机主控板 | +| K210 识别端 | `k210/` | 摄像头采集、人脸检测、人脸特征比对、添加/删除本地人脸、上传人脸照片、同步人脸映射 | 摄像头和 AI 识别模块 | +| C++ 服务端 | `server/` | 提供 HTTP API、保存考勤记录、保存配置、管理照片和同步状态 | 中心服务器 | +| Qt 管理端 | `QT/` | Windows/Android 图形管理界面,用于查看记录、统计、配置时间段、管理映射和照片 | 管理后台 | + +整个系统的目标是: + +1. K210 看到人脸并识别出一个人。 +2. K210 通过串口把识别结果发给 STM32。 +3. STM32 判断当前是否在允许签到的时间段内,并检查是否重复签到。 +4. 如果允许签到,STM32 将签到数据用 AES-128-CBC 加密后通过 ESP8266 发给服务端。 +5. 服务端保存数据,并提供给 Qt 管理端查看、统计、导出和管理。 +6. Qt 管理端修改的配置会同步给服务端,再由 STM32 或 K210 拉取。 + +## 2. 新手先理解几个概念 + +### 2.1 人脸 ID 和学号不是同一个东西 + +K210 本地添加人脸时,会生成一个原始人脸编号,例如 `ID:1`、`ID:2`。这个编号只表示 K210 本地第几个录入的人脸。 + +真实考勤需要的是学号,例如 `20240001`。所以项目里有一个“人脸 ID 到学号”的映射: + +```json +{ + "mappings": { + "1": "20240001", + "2": "20240002" + } +} +``` + +含义是:K210 本地人脸 `1` 对应真实学号 `20240001`。 + +### 2.2 签到时间段默认是空的 + +当前代码中,系统启动时不再自动创建“上午签到”“中午签退”“下午签到”“晚上签退”这四个默认时间段。 + +也就是说,新部署时: + +```json +{ + "time_slots": [] +} +``` + +如果没有配置时间段,STM32 会认为当前不在任何考勤时段内,识别到人脸也不会允许签到。需要在 Qt 管理端“配置中心”里新增时间段并保存同步。 + +### 2.3 服务端是数据中心 + +当前服务端以 SQLite 数据库为主,数据库文件是: + +```text +attendance.db +``` + +服务端还会使用以下本地资源: + +| 文件或目录 | 作用 | +| --- | --- | +| `attendance.db` | 主数据库,保存记录、配置、映射、同步状态 | +| `faces/` | 人脸照片目录,保存 K210 上传的 `.jpg` | +| `backups/` | 数据库备份目录 | +| `attendance_database.csv` | 旧版本记录文件,首次初始化时可导入 | +| `timeslots_config.json` | 旧版本时间段配置,首次初始化时可导入 | +| `mapping_config.json` | 旧版本姓名/班级映射,首次初始化时可导入 | +| `face_id_mapping.json` | 旧版本人脸映射,首次初始化时可导入 | +| `pending_delete.json` | 旧版本待删除人脸列表,首次初始化时可导入 | +| `sync_request.json` | 旧版本同步请求标志,首次初始化时可导入 | +| `sync_version.txt` | 旧版本同步版本号,首次初始化时可导入 | + +### 2.4 同步版本号是什么 + +服务端维护一个全局同步版本号 `sync_version`。当考勤记录、时间段、映射、人脸照片、删除任务等数据发生变化时,服务端会把版本号加一。 + +设备同步时会带上自己上次同步到的版本: + +```text +GET /api/v1/sync/state?client=stm32&last_version=3 +``` + +如果服务端当前版本还是 `3`,会返回 `changed:false`。如果服务端当前版本是 `4` 或更高,会返回 `changed:true` 并带上该设备需要的数据。 + +这样做的好处是:设备不用不停下载所有数据,只要版本没变就不用重新处理。 ## 3. 总体架构 -1. K210 识别人脸,经 UART 上报 STM32。 -2. STM32 执行签到策略并加密上传服务端 `POST /api/v1/attendance/checkins`。 -3. 服务端写入 `attendance.db` 并提供管理 API。 -4. 服务端在 SQLite 中维护全局同步版本,作为各端同步的权威版本。 -5. Qt 管理端通过统一同步接口拉取记录/统计、配置、人脸照片和映射。 -6. STM32 与 K210 分别按本地 `last_version` 增量拉取时间段、人脸映射与待删除任务,保持端云一致。 -## 4. 文档索引(分端) -- STM32 固件端:`docs/01_STM32固件端文档.md` -- K210 识别端:`docs/02_K210识别端文档.md` -- 服务端:`docs/03_服务端文档.md` -- Qt 管理端:`docs/04_Qt管理端文档.md` -- 项目审查问题清单:`docs/05_项目审查问题清单.md` +```mermaid +flowchart LR + Camera["摄像头"] --> K210["K210 识别端"] + K210 -->|"UART1: ID, NAME, CONF"| STM32["STM32 主控"] + STM32 -->|"USART1 AT 指令"| ESP1["ESP8266"] + ESP1 -->|"HTTP + AES 签到"| Server["C++ 服务端 8082"] + K210 -->|"UART2 AT 指令"| ESP2["K210 侧 ESP8266"] + ESP2 -->|"HTTP 上传照片/同步映射"| Server + Qt["Qt 管理端 Windows/Android"] -->|"HTTP API"| Server + Server --> SQLite["attendance.db"] + Server --> Faces["faces/*.jpg"] +``` -## 5. 各端关键配置位置 -- STM32 网络与班级配置:`User/main.c` -- K210 WiFi/服务器配置:`k210/main.py` -- Qt 默认服务地址与 API 封装:`QT/src/ApiClient.cpp`、`QT/src/AppController.cpp` -- 服务端端口与数据文件:`server/server_encrypted.cpp` +## 4. 数据流说明 -## 6. 一致性要求 -- AES Key/IV 必须在 STM32、服务端、Qt 三端一致。 -- 服务端建议统一使用加密版本,避免接口行为不一致。 -- 各端时间配置需统一时区(当前代码以 UTC+8 逻辑处理)。 +### 4.1 正常签到流程 -## 7. 快速部署顺序 -1. 编译并启动服务端(确认 `:8082` 可访问)。 -2. 启动 Qt,先做 `/api/v1/system/health` 连通性验证。 -3. 烧录 STM32 固件并联网。 -4. 启动 K210 脚本并确认识别/同步正常。 -5. 用 Qt 检查签到记录、照片、映射与删除同步链路。 +1. K210 摄像头采集画面。 +2. K210 使用三个 KPU 模型执行人脸检测、关键点定位和特征提取。 +3. K210 将当前人脸特征与 SD 卡中的 `recordftr.txt` 做比对。 +4. 如果分数大于阈值,K210 认为识别成功。 +5. K210 通过 UART1 发送给 STM32: + +```text +ID:20240001,NAME:20240001,CONF:0.95 +``` + +6. STM32 解析串口数据。 +7. STM32 检查置信度是否超过 `0.75`。 +8. STM32 检查当前时间是否落在已启用的时间段内。 +9. STM32 检查本地缓存、今日记录缓存和服务端查重结果,避免同一个学生在同一时段重复签到。 +10. STM32 生成 JSON: + +```json +{ + "student_id": "20240001", + "name": "20240001", + "confidence": 0.95, + "status": "success", + "class_id": "1" +} +``` + +11. STM32 使用 AES-128-CBC 加密并做 Base64 编码。 +12. STM32 通过 ESP8266 发送: + +```text +POST /api/v1/attendance/checkins +X-Api-Token: ... +X-Encrypted: AES128-CBC +``` + +13. 服务端解密、校验字段、写入 SQLite。 +14. Qt 管理端刷新后可以看到新记录。 + +### 4.2 新增人脸流程 + +1. 在 K210 正常识别界面短按按键,进入添加人脸模式。 +2. 摄像头检测到陌生人脸后,再短按确认。 +3. K210 将人脸特征保存到 SD 卡 `recordftr.txt`。 +4. K210 将对应照片保存到 `/sd/face_photos/.jpg`。 +5. K210 将照片 ID 放入上传队列。 +6. 网络任务空闲时,K210 通过 ESP8266 上传照片到服务端: + +```text +POST /api/v1/faces/photos/ +``` + +7. Qt 管理端进入“人脸管理”,刷新后可以看到照片。 +8. 管理员为该 face_id 填写真实学号并保存映射。 +9. 服务端保存映射并触发同步。 +10. K210 拉取同步状态后,把本地人脸名称从 `ID:` 更新为真实学号。 + +### 4.3 删除人脸流程 + +1. Qt 管理端删除某个学生或人脸。 +2. 服务端删除照片、映射,按需要删除考勤记录。 +3. 服务端写入待删除任务 `pending_delete`,并提升同步版本。 +4. K210 定期检查同步请求或拉取统一同步状态。 +5. K210 在本地删除对应特征和照片。 +6. K210 向服务端发送 ACK: + +```text +POST /api/v1/sync/ack +``` + +7. 服务端移除待删除任务。 + +## 5. 目录结构 + +```text +Reconstruct_Qt/ +├── docs/ # 项目文档 +├── server/ +│ ├── server_encrypted.cpp # C++ HTTP 服务端,当前推荐版本 +│ ├── regression_api_tests.ps1 # Windows API 回归检查脚本 +│ └── tiny_aes.hpp # AES 辅助代码 +├── QT/ +│ ├── CMakeLists.txt # Qt 客户端构建配置 +│ ├── main.cpp # Qt 程序入口 +│ ├── qml/Main.qml # Qt 主界面 +│ ├── src/ # Qt C++ 业务逻辑、网络客户端、模型 +│ ├── android/ # Android 打包配置和图标 +│ └── assets/ # 桌面端图标 +├── k210/ +│ ├── main.py # K210 主程序 +│ └── view_attendance.py # 旧版/辅助查看本地记录脚本 +├── User/ +│ └── main.c # STM32 主循环 +├── Hardware/ # STM32 外设驱动与业务模块 +├── System/ # STM32 延时、调试等基础模块 +├── Library/ # STM32 标准外设库 +├── Start/ # 启动文件和芯片头文件 +├── Project.uvprojx # Keil 工程文件 +├── Project.uvoptx # Keil 选项文件 +└── 连接图.md # 硬件接线表 +``` + +## 6. 默认配置总览 + +| 配置项 | 当前值 | 所在位置 | +| --- | --- | --- | +| 服务端端口 | `8082` | `server/server_encrypted.cpp` | +| 服务端默认 API Token | `atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV` | 服务端、Qt、STM32、K210 | +| 服务端默认 CORS 来源 | `http://localhost` | `server/server_encrypted.cpp` | +| STM32 WiFi SSID | `yzfh` | `User/main.c` | +| STM32 WiFi 密码 | `yzfhroot` | `User/main.c` | +| STM32 服务端 IP | `27.106.113.205` | `User/main.c` | +| STM32 班级编号 | `1` | `User/main.c` | +| K210 WiFi SSID | `yzfh` | `k210/main.py` | +| K210 WiFi 密码 | `yzfhroot` | `k210/main.py` | +| K210 服务端 IP | `27.106.113.205` | `k210/main.py` | +| Qt 默认服务端地址 | `http://127.0.0.1:8082` | `QT/src/ApiClient.cpp`、`QT/src/AppController.cpp` | +| 默认签到时间段 | 空数组 | Qt、服务端、STM32 | +| AES 算法 | AES-128-CBC + PKCS#7 + Base64 | `Hardware/Crypto.c`、`server/server_encrypted.cpp`、`QT/src/CryptoUtils.cpp` | + +注意:这些配置里包含真实网络信息和默认 Token。如果要部署到公网,建议改成自己的值,并用环境变量覆盖服务端和 Qt 的 Token。 + +## 7. 硬件连接概览 + +完整连接表在根目录 `连接图.md`。常用连接如下: + +| 模块 | STM32 引脚 | 功能 | +| --- | --- | --- | +| ESP8266 | PA9 / PA10 | USART1,STM32 通过 AT 指令访问 WiFi 和 HTTP | +| K210 | PB10 / PB11 | USART3,K210 把识别结果发给 STM32 | +| OLED | PB8 / PB9 | I2C 显示 | +| 调试串口 | PA2 / PA3 | USART2 调试输出 | +| 成功 LED | PB4 | 高电平点亮 | +| 蜂鸣器 | PB5 | 低电平触发 | +| 光敏传感器 | PB0 | ADC 输入 | +| 补光灯 | PB1 | 高电平点亮 | +| DS1302 CLK/DAT/RST | PA4 / PA5 / PA6 | 软件模拟时序 | + +K210 侧: + +| 模块 | K210 引脚 | 功能 | +| --- | --- | --- | +| STM32 UART | 24 / 25 | UART1,发送识别结果给 STM32 | +| ESP8266 UART | 34 / 35 | UART2,上传照片和同步配置 | +| 按键 | 16 | 短按添加,长按删除,超长按删除全部 | +| RGB LED | 12 / 13 / 14 | 状态提示 | + +## 8. 加密与鉴权 + +### 8.1 API Token + +除少数公开接口外,服务端要求请求头中带 Token: + +```text +X-Api-Token: atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV +``` + +服务端可用环境变量覆盖: + +```powershell +$env:ATTENDANCE_API_TOKEN="你的新Token" +``` + +Qt 端也会读取同名环境变量;STM32 和 K210 目前是写在代码里的常量,需要改代码后重新烧录或上传脚本。 + +### 8.2 AES 参数 + +三端必须保持完全一致: + +| 参数 | 值 | +| --- | --- | +| 算法 | AES-128-CBC | +| 填充 | PKCS#7 | +| 编码 | Base64 | +| Key | `2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c` | +| IV | `00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f` | + +STM32 上传签到使用加密请求。Qt 当前普通记录和统计请求使用 JSON 明文响应,代码仍保留了解密能力。 + +## 9. 推荐启动顺序 + +1. 启动服务端,确认 `http://服务器IP:8082/api/v1/system/health` 返回健康状态。 +2. 启动 Qt 管理端,在配置中心填写服务端地址并点击“测试连接”。 +3. 在 Qt 管理端新增签到时间段并保存。 +4. 烧录并启动 STM32,确认 OLED 显示系统就绪。 +5. 启动 K210,确认摄像头画面、模型加载和 WiFi 连接正常。 +6. 在 K210 添加人脸,等待照片上传到服务端。 +7. 在 Qt 管理端为照片对应的人脸 ID 填写真实学号并保存映射。 +8. K210 同步映射后,再让同一人进行识别签到。 +9. 在 Qt 管理端查看考勤记录和统计结果。 + +## 10. 一键理解各端关系 + +| 你想做的事 | 应该操作哪里 | 背后发生什么 | +| --- | --- | --- | +| 改服务端地址 | Qt 配置中心、STM32/K210 代码常量 | 各端 HTTP 请求指向新服务器 | +| 设置允许签到的时间 | Qt 配置中心 | Qt 保存到服务端,STM32 定期下载 | +| 给人脸绑定学号 | Qt 人脸管理 | 服务端保存映射,K210 同步后更新本地人脸名称 | +| 添加人脸 | K210 按键短按 | K210 保存特征和照片,照片上传服务端 | +| 删除人脸 | Qt 人脸管理或 K210 删除模式 | Qt 删除会生成服务端同步任务;K210 本地删除只影响本地 | +| 查看记录 | Qt 考勤记录页 | Qt 从服务端读取最近记录 | +| 导出报表 | Qt 记录页或统计页 | Qt 把当前加载/筛选结果写成 CSV | + +## 11. 新手常见问题 + +### 11.1 为什么识别成功但没有签到成功 + +常见原因: + +1. 没有配置签到时间段。 +2. 当前时间不在任何启用的时间段内。 +3. STM32 时间不正确,DS1302 失效且网络对时失败。 +4. 同一学生在当前时间段已经签过。 +5. K210 发给 STM32 的 ID 不是想要的真实学号。 +6. 置信度低于 `0.75`。 +7. STM32 无法连接服务端,记录暂存在本地等待同步。 + +### 11.2 为什么 Qt 看不到 K210 的照片 + +可能原因: + +1. K210 侧 ESP8266 没连上 WiFi。 +2. K210 服务端 IP 或 Token 不正确。 +3. K210 本地照片保存在 `/sd/face_photos`,但还没轮到网络队列上传。 +4. 服务端 `faces/` 目录没有写入权限。 +5. Qt 配置的服务端地址不是 K210 上传照片的那个服务端。 + +### 11.3 为什么删除后 K210 还认识这个人 + +Qt 删除会先在服务端生成待删除任务,K210 需要同步后才会删除本地 SD 卡中的特征。可以等待 K210 定期同步,也可以通过服务端同步请求接口触发。 + +### 11.4 为什么改了 Token 后有的端还能用,有的端不能用 + +服务端和 Qt 可以读取环境变量 `ATTENDANCE_API_TOKEN`。STM32 与 K210 的 Token 写在代码里,必须同步修改并重新烧录或上传脚本。 + +## 12. 文档索引 + +| 文档 | 内容 | +| --- | --- | +| `docs/00_项目总文档.md` | 总体介绍、架构、运行顺序、概念说明 | +| `docs/01_STM32固件端文档.md` | STM32 固件、硬件模块、签到判断、加密上传 | +| `docs/02_K210识别端文档.md` | K210 人脸识别、按键操作、照片上传、同步 | +| `docs/03_服务端文档.md` | 服务端 API、SQLite、同步版本、部署安全 | +| `docs/04_Qt管理端文档.md` | Qt 界面功能、数据模型、本地缓存、PC/Android 适配 | +| `docs/05_项目审查问题清单.md` | 当前实现状态、风险点、维护清单 | +| `docs/06_后端接口Windows测试命令.md` | Windows 下手动测试和回归检查命令 | diff --git a/docs/01_STM32固件端文档.md b/docs/01_STM32固件端文档.md index da5f326..4030e3e 100644 --- a/docs/01_STM32固件端文档.md +++ b/docs/01_STM32固件端文档.md @@ -1,66 +1,337 @@ -# STM32固件端文档 +# STM32 固件端文档 -## 1. 端侧职责 -- 从 K210 串口接收识别结果。 -- 在本地执行签到逻辑(时间段判断、重复判定、缓存与重传)。 -- 通过 ESP8266 将签到数据以 AES-128-CBC 加密后上传服务端。 -- 维护 RTC(DS1302)与网络时间同步。 +## 1. STM32 在系统中做什么 -对应入口文件:`User/main.c` +STM32 是整套考勤机的主控。它不做人脸识别,人脸识别由 K210 完成。STM32 负责把 K210 的识别结果变成“是否允许签到”的业务判断,并控制硬件反馈。 -## 2. 关键模块 -- 主流程:`User/main.c` -- 考勤逻辑:`Hardware/Attendance.c` -- WiFi/HTTP:`Hardware/esp8266.c` -- 加密:`Hardware/Crypto.c` -- RTC:`Hardware/DS1302.c` -- 光照补光:`Hardware/LightSensor.c` -- K210串口解析:`Hardware/K210_USART.c` +它主要做这些事: -## 3. 当前默认配置(代码内) -文件:`User/main.c` -- `WIFI_SSID`: `YOUR_WIFI_SSID` -- `WIFI_PASSWORD`: `YOUR_WIFI_PASSWORD` -- `HTTP_SERVER_IP`: `127.0.0.1`(部署时改为实际服务端地址) -- `HTTP_SERVER_PORT`: `8082` -- `HTTP_API_TOKEN`: `change-me-token`(部署时必须修改) -- `CLASS_ID`: `1` +1. 初始化 OLED、蜂鸣器、LED、串口、RTC、光敏传感器、补光灯等硬件。 +2. 接收 K210 通过 USART3 发来的识别结果。 +3. 判断识别置信度是否足够。 +4. 判断当前时间是否在服务端配置的签到时间段内。 +5. 判断当前学生在本时间段是否已经签过。 +6. 生成签到记录。 +7. 使用 AES-128-CBC 加密签到 JSON。 +8. 通过 ESP8266 发送 HTTP 请求到服务端。 +9. 网络异常时把记录留在本地,后续自动重试。 -建议将上述配置改为部署环境参数后再编译烧录。 +## 2. 相关文件 -## 4. 通信与接口 -STM32 通过 `ESP8266_HttpGet` / `ESP8266_SendEncryptedData` 访问服务端: -- `POST /api/v1/attendance/checkins`:上传加密签到数据(必带 `X-Encrypted: AES128-CBC`)。 -- `GET /api/v1/system/time`:获取服务器时间戳。 -- `GET /api/v1/sync/state?client=stm32&last_version=`:按版本同步时间段配置。 -- `GET /api/v1/attendance/duplicates`:按时段查重。 -- `GET /api/v1/attendance/today`:下载今日已签到学号列表。 +| 文件 | 作用 | +| --- | --- | +| `User/main.c` | 主程序入口、初始化流程、主循环、联网和同步调度 | +| `Hardware/Attendance.c` | 考勤业务逻辑:时间段、重复判断、本地记录、同步状态 | +| `Hardware/Attendance.h` | 考勤数据结构和函数声明 | +| `Hardware/K210_USART.c` | 解析 K210 串口识别结果 | +| `Hardware/esp8266.c` | ESP8266 AT 指令、HTTP GET/POST、对时、查重 | +| `Hardware/Crypto.c` | AES-128-CBC 加密和 Base64 编码 | +| `Hardware/DS1302.c` | DS1302 RTC 时钟读写 | +| `Hardware/LightSensor.c` | 光敏电阻 ADC 和补光灯控制 | +| `Hardware/Buzzer_LED.c` | 蜂鸣器、成功 LED、状态 LED | +| `Hardware/OLED.c` | OLED 显示 | +| `System/Delay.c` | 延时和毫秒计时 | +| `System/Debug.c` | 调试串口输出 | +| `Project.uvprojx` | Keil 工程文件 | -## 5. 主流程说明 -1. 系统初始化(OLED、串口、考勤模块、加密模块、RTC、传感器)。 -2. 初始化并连接 ESP8266 到 WiFi 与 HTTP 服务器。 -3. 可用时同步网络时间到 DS1302。 -4. 定时拉取时间段配置。 -5. 处理 K210 上报的人脸识别结果: -- 置信度通过阈值后进入签到逻辑。 -- 本地去重 + 服务器时段去重。 -- 记录加入本地队列并尝试上传。 -6. 网络异常时离线缓存,恢复后批量补传。 +## 3. 硬件连接 -## 6. 调试建议 -- 优先看串口调试输出(`DEBUG_PRINT` 系列)。 -- 检查 `g_wifi_connected`、`g_server_connected` 变化。 -- 若频繁重连,先确认 AP 稳定性、服务器可达性与 8082 端口。 +| 外设 | STM32 引脚 | 说明 | +| --- | --- | --- | +| ESP8266 TX/RX | PA10 / PA9 | USART1,波特率 115200 | +| K210 TX/RX | PB11 / PB10 | USART3,波特率 115200 | +| 调试串口 | PA3 / PA2 | USART2,波特率 115200 | +| OLED SCL/SDA | PB8 / PB9 | I2C | +| 成功 LED | PB4 | 高电平点亮 | +| 蜂鸣器 | PB5 | 低电平触发 | +| 光敏传感器 | PB0 | ADC1 通道 8 | +| 补光灯 | PB1 | 高电平点亮 | +| DS1302 CLK/DAT/RST | PA4 / PA5 / PA6 | 软件 GPIO 时序 | +| 板载状态 LED | PC13 | 低电平点亮,初始化中亮,就绪后灭 | -## 7. 构建与烧录 -工程为 Keil 工程: -- `Project.uvprojx` -- `Project.uvoptx` +## 4. 代码中的主要配置 -按当前板卡配置在 Keil 中编译并烧录。 +位置:`User/main.c` -## 8. 依赖与注意事项 -- 与服务端 AES Key/IV 必须一致(否则 `/api/v1/attendance/checkins` 解密失败)。 -- DS1302 可用时优先作为本地时钟;不可用则回退网络时间。 -- `Attendance.c` 的默认时间段仅为兜底,实际以服务器下发为准。 +| 配置 | 当前值 | 说明 | +| --- | --- | --- | +| `WIFI_SSID` | `yzfh` | ESP8266 要连接的 WiFi | +| `WIFI_PASSWORD` | `yzfhroot` | WiFi 密码 | +| `HTTP_SERVER_IP` | `27.106.113.205` | 服务端 IP | +| `HTTP_SERVER_PORT` | `8082` | 服务端 HTTP 端口 | +| `HTTP_API_TOKEN` | `atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV` | API Token | +| `CLASS_ID` | `1` | 当前 STM32 设备所属班级编号 | +| `TIME_SYNC_INTERVAL_MS` | `3600000` | 每小时对时一次 | +| `TIMESLOT_DOWNLOAD_INTERVAL_MS` | `30000` | 每 30 秒检查一次时间段配置 | +| `WIFI_RECONNECT_INTERVAL_MS` | `30000` | WiFi 断开后每 30 秒重连 | +| `RECONNECT_INTERVAL_MS` | `5000` | 服务端连接不可用后每 5 秒尝试重连 | +| `MIN_CONFIDENCE_THRESHOLD` | `0.75` | 最低识别置信度 | +| `MAX_ATTENDANCE_RECORDS` | `100` | STM32 本地最多缓存 100 条记录 | +| `MAX_TIME_SLOT_COUNT` | `10` | 最多支持 10 个签到时间段 | +## 5. 启动流程 + +STM32 上电后按下面顺序执行: + +1. 初始化调试串口。 +2. 初始化延时模块。 +3. 初始化 OLED。 +4. 初始化蜂鸣器和 LED。 +5. 初始化 USART1,用于连接 ESP8266。 +6. 初始化 USART3,用于接收 K210 数据。 +7. 初始化考勤模块。 +8. 初始化 AES 加密模块。 +9. 初始化 DS1302,并尝试读取时间。 +10. 初始化光敏传感器和补光灯。 +11. 初始化 ESP8266。 +12. 连接 WiFi。 +13. 从服务端获取当前时间。 +14. 连接 HTTP 服务端。 +15. 下载签到时间段配置。 +16. 下载今日当前时段已签到记录。 +17. 进入主循环。 + +如果 WiFi 或服务端连接失败,程序不会停机,而是进入离线或重连状态。后续主循环会自动重试。 + +## 6. 主循环每次做什么 + +`User/main.c` 的主循环大致如下: + +```c +while (1) { + ProcessBeep(); + UpdateCurrentTime(); + CheckServerConnection(); + SyncPendingRecords(); + UpdateTimeSlotConfig(); + LightSensor_Update(); + + if (K210_GetRxFlag()) { + Attendance_ProcessFaceResult(&K210_FaceResult); + } +} +``` + +逐项解释: + +| 函数 | 作用 | +| --- | --- | +| `ProcessBeep` | 非阻塞处理蜂鸣器和成功 LED | +| `UpdateCurrentTime` | 从 DS1302 或软件时钟维护当前时间,并定期网络对时 | +| `CheckServerConnection` | 检查当前 HTTP/TCP 连接状态,连接不可用时按需重连 | +| `SyncPendingRecords` | 把本地未同步记录加密上传 | +| `UpdateTimeSlotConfig` | 定期下载服务端签到时间段 | +| `LightSensor_Update` | 根据环境光自动开关补光灯 | +| `K210_GetRxFlag` | 看是否收到 K210 的新识别结果 | + +注意:当前 STM32 HTTP 请求和服务端响应都使用 `Connection: close`,所以它不是长期保持一个 TCP 长连接,而是在每次请求前检查连接状态,并在需要时重新建立连接。 + +## 7. K210 发给 STM32 的数据格式 + +K210 通过 UART1 发送文本,STM32 通过 USART3 接收。 + +识别成功: + +```text +ID:20240001,NAME:Zhang_San,CONF:0.95 +``` + +字段说明: + +| 字段 | 含义 | +| --- | --- | +| `ID` | 学号或 K210 同步后的身份编号 | +| `NAME` | 显示名,K210 代码中会用下划线代替空格 | +| `CONF` | 置信度,范围一般是 0.0 到 1.0 | + +未识别: + +```text +UNKNOWN +``` + +STM32 收到 `UNKNOWN` 后不会签到。 + +## 8. 签到判断逻辑 + +STM32 对一次识别结果的判断顺序如下: + +1. 判断 K210 是否识别成功。 +2. 判断置信度是否低于 `0.75`。 +3. 判断当前是否在启用的时间段内。 +4. 判断当前时段本地记录中是否已经有该学生。 +5. 判断从服务端下载的今日缓存中是否已经有该学生。 +6. 如果服务端在线,再调用服务端查重接口确认。 +7. 通过所有检查后,生成一条本地考勤记录。 +8. 如果服务端在线,后续同步任务会加密上传。 + +当前默认没有任何时间段。必须由 Qt 管理端新增并保存时间段,否则第 3 步会失败,OLED 会显示不在签到时间。 + +## 9. 时间段同步 + +STM32 调用: + +```text +GET /api/v1/sync/state?client=stm32&last_version=<本地版本> +``` + +服务端返回示例: + +```json +{ + "version": 5, + "changed": true, + "client": "stm32", + "server_time": "2026-05-01 10:00:00", + "state": { + "time_slots": [ + { + "name": "上午签到", + "start_time": "08:00", + "end_time": "09:30", + "enabled": true + } + ] + } +} +``` + +如果 `time_slots` 是空数组,STM32 会清空本地时间段配置。 + +## 10. 今日记录缓存 + +STM32 为了减少重复签到,会在开机连接服务端后下载今日当前时间段的已签到学号: + +```text +GET /api/v1/attendance/today?slot_start=08:00&slot_end=09:30 +``` + +返回: + +```json +{ + "date": "2026-05-01", + "records": ["20240001", "20240002"] +} +``` + +下载后,STM32 会把这些学号放进缓存。之后如果同一个学生在同一个时段再次识别,会被判定为重复签到。 + +## 11. 加密上传 + +STM32 上传签到记录时使用: + +```text +POST /api/v1/attendance/checkins +X-Api-Token: +X-Encrypted: AES128-CBC +Content-Type: application/octet-stream +``` + +请求体是 Base64 编码后的 AES 密文。明文 JSON 类似: + +```json +{ + "student_id": "20240001", + "name": "Zhang San", + "confidence": 0.95, + "status": "success", + "class_id": "1" +} +``` + +服务端解密成功后写入 `attendance.db`。 + +## 12. 本地记录和同步状态 + +STM32 的本地记录结构体: + +```c +typedef struct { + char student_id[20]; + char student_name[32]; + float confidence; + uint32_t timestamp; + uint8_t time_slot; + SyncStatus sync_status; + uint8_t retry_count; + uint32_t last_retry_time; +} AttendanceRecord; +``` + +同步状态: + +| 状态 | 含义 | +| --- | --- | +| `SYNC_PENDING` | 新记录,等待上传 | +| `SYNC_SYNCING` | 正在上传 | +| `SYNC_SYNCED` | 上传成功 | +| `SYNC_FAILED` | 上传失败,后续重试 | + +## 13. OLED 和声光反馈 + +| 情况 | OLED | 蜂鸣器/LED | +| --- | --- | --- | +| 初始化中 | 显示 ESP/WiFi/HTTP 状态 | PC13 状态灯亮 | +| 服务端在线 | `System Ready / Online Mode` | 成功提示 | +| WiFi 在线但服务端断开 | `System Ready / Reconnecting..` | 后台重连 | +| WiFi 正在重连 | `System Ready / WiFi retry...` | 后台重连 | +| ESP8266 不可用 | `System Ready / Offline Mode` | 错误提示 | +| 签到成功 | `Check-in OK!` | 成功提示 | +| 重复签到 | `Already Signed!` | 错误提示 | +| 置信度低 | `Low Confidence` | 错误提示 | +| 不在时间段 | `Not Check Time` | 错误提示 | + +## 14. 编译与烧录 + +使用 Keil 打开: + +```text +Project.uvprojx +``` + +一般步骤: + +1. 安装 Keil MDK 和 STM32F10x 支持包。 +2. 打开 `Project.uvprojx`。 +3. 确认芯片型号和启动文件与实际开发板一致。 +4. 修改 `User/main.c` 中的 WiFi、服务端 IP、班级编号和 Token。 +5. 编译工程。 +6. 使用 ST-Link 或串口 Bootloader 烧录。 +7. 打开调试串口查看日志。 + +## 15. 调试建议 + +### 15.1 先看调试串口 + +调试串口会输出: + +1. 硬件初始化状态。 +2. ESP8266 AT 指令结果。 +3. WiFi 连接结果。 +4. HTTP 服务端连接结果。 +5. 时间同步结果。 +6. 时间段下载结果。 +7. K210 原始数据和解析结果。 +8. 加密上传结果。 + +### 15.2 常见问题 + +| 现象 | 可能原因 | 处理 | +| --- | --- | --- | +| 一直显示 WiFi retry | SSID/密码错误、ESP8266 没接好、供电不足 | 检查 `WIFI_SSID`、`WIFI_PASSWORD`、供电和串口 | +| HTTP connection failed | 服务端 IP/端口错误、防火墙拦截、服务端没启动 | 用电脑访问健康检查接口 | +| Not Check Time | 没有配置时间段,或当前时间不在时间段内 | Qt 新增时间段并保存,检查 DS1302/NTP | +| Already Signed | 当前时段已经签过 | 换时间段或清空记录 | +| Low Confidence | K210 比对分数偏低 | 调整光线、重新录入人脸 | +| Qt 看不到记录 | STM32 未同步成功或服务端地址不一致 | 看服务端日志和 Qt 服务端地址 | + +## 16. 修改配置时要同步修改哪里 + +| 你修改了什么 | 需要同步修改 | +| --- | --- | +| 服务端 IP | `User/main.c`、`k210/main.py`、Qt 配置中心 | +| API Token | 服务端环境变量或代码、`User/main.c`、`k210/main.py`、Qt 环境变量 | +| AES Key/IV | `Hardware/Crypto.c`、`server/server_encrypted.cpp`、`QT/src/CryptoUtils.cpp` | +| 班级编号 | 每块 STM32 对应修改 `CLASS_ID` | +| WiFi | `User/main.c` 和 `k210/main.py` | diff --git a/docs/02_K210识别端文档.md b/docs/02_K210识别端文档.md index 4ada3f0..bc9981f 100644 --- a/docs/02_K210识别端文档.md +++ b/docs/02_K210识别端文档.md @@ -1,75 +1,293 @@ -# K210识别端文档 +# K210 识别端文档 -## 1. 端侧职责 -- 进行人脸检测、特征提取、识别。 -- 通过 UART1 将识别结果实时发送给 STM32。 -- 通过 UART2 驱动 ESP8266,与服务端同步人脸映射与删除任务。 -- 上传新增人脸照片到服务端。 +## 1. K210 在系统中做什么 -对应入口文件:`k210/main.py` +K210 负责“看见人脸并判断是谁”。它不直接决定能不能签到,最终签到判断由 STM32 完成。 -## 2. 通信链路 -- `UART1`(K210 <-> STM32):发送识别结果。 -- `UART2`(K210 <-> ESP8266):HTTP 通信到服务端 `:8082`。 +K210 的职责包括: -识别结果发送格式(给 STM32): -- 已识别:`ID:,NAME:,CONF:` -- 陌生人:`UNKNOWN` +1. 初始化摄像头和 LCD。 +2. 加载人脸检测、关键点检测、特征提取三个 KPU 模型。 +3. 从 SD 卡读取已经录入的人脸特征。 +4. 实时检测画面中的人脸。 +5. 将识别结果通过 UART1 发给 STM32。 +6. 通过按键添加或删除本地人脸。 +7. 保存新增人脸照片到 SD 卡。 +8. 通过 K210 侧 ESP8266 上传照片到服务端。 +9. 从服务端同步人脸 ID 到学号的映射。 +10. 从服务端同步待删除任务,并删除本地特征。 -## 3. 本地数据文件 -- 人脸特征库:`/sd/recordftr.txt` -- 本地映射缓存:`/sd/face_mapping.txt` -- 本地同步版本:`/sd/sync_version.txt` -- 本地人脸照片缓存:`/sd/face_photos/.jpg` +## 2. 相关文件 -## 4. 服务端交互接口 -K210 在脚本中使用 HTTP 访问以下接口: -- `GET /api/v1/sync/state?client=k210&last_version=<本地版本>`:按版本拉取统一同步状态。 -- `POST /api/v1/sync/ack`:确认已完成删除任务。 -- `GET /api/v1/sync/request`:轮询是否触发同步。 -- `POST /api/v1/sync/request/ack`:确认收到同步请求。 -- `GET /api/v1/faces/photos`:获取服务端已有照片列表,用于判断本地照片是否需要补传。 -- `POST /api/v1/faces/photos/`:上传 JPEG 人脸照片。 +| 文件 | 作用 | +| --- | --- | +| `k210/main.py` | K210 主程序,包含识别、添加、删除、同步、上传照片 | +| `k210/view_attendance.py` | 辅助脚本,用于查看旧版本地记录文件 | -v1 兼容辅助接口: -- `GET /api/v1/faces/mappings` -- `GET /api/v1/faces/pending-delete` -- `POST /api/v1/faces/delete-ack` +K210 程序依赖 SD 卡上的模型文件: -## 5. 启动与运行流程 -1. 初始化摄像头、KPU 模型、LCD、按键中断、串口。 -2. 启动 ESP8266,尝试连接 WiFi。 -3. 启动同步: -- 读取 `/sd/sync_version.txt`。 -- 请求统一同步状态。 -- 先删除 `pending_delete` 中的人脸特征。 -- 再应用 `face_mappings`。 -- 成功后保存新的同步版本。 -4. 进入主循环:识别 + 绘制 + 向 STM32 上报。 -5. 分时网络任务(避免阻塞识别帧率): -- 新增人脸时先把 JPEG 保存到 `/sd/face_photos/`,再加入上传队列。 -- 启动时先获取服务端照片列表,再扫描 `/sd/face_photos/`,只补传服务端缺失的照片;如果服务端不可达,则退回全量加入补传队列。 -- 优先处理照片上传队列,上传失败会保留本地照片并等待下次重试。 -- 然后处理同步任务。 +```text +/sd/FaceDetection.smodel +/sd/FaceLandmarkDetection.smodel +/sd/FeatureExtraction.smodel +``` -## 6. 按键功能 -在 `k210/main.py` 中定义: -- 短按:添加人脸。 -- 长按(约2秒):进入删除单个人脸模式。 -- 超长按(约5秒):删除全部人脸。 +如果模型文件不存在或路径错误,程序会在加载 KPU 模型时报错。 -## 7. 当前默认配置(代码内) -- WiFi:`YOUR_WIFI_SSID / YOUR_WIFI_PASSWORD` -- 服务端:`127.0.0.1:8082`(部署时改为实际服务端地址) -- API Token:`change-me-token`(部署时必须修改) +## 3. 代码中的主要配置 -文件位置:`k210/main.py` +位置:`k210/main.py` -## 8. 部署注意事项 -- SD 卡需包含模型文件: - - `/sd/FaceDetection.smodel` - - `/sd/FaceLandmarkDetection.smodel` - - `/sd/FeatureExtraction.smodel` -- 与 STM32 使用统一学号语义(映射同步后应发送真实学号语义)。 -- 网络任务失败不会阻塞识别主流程,但会影响照片与映射同步时效;本地照片缓存会用于后续补传。 +| 配置 | 当前值 | 说明 | +| --- | --- | --- | +| `WIFI_SSID` | `yzfh` | K210 侧 ESP8266 要连接的 WiFi | +| `WIFI_PASSWORD` | `yzfhroot` | WiFi 密码 | +| `SERVER_IP` | `27.106.113.205` | 服务端 IP | +| `SERVER_PORT` | `8082` | 服务端端口 | +| `API_TOKEN` | `atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV` | 服务端鉴权 Token | +| `SYNC_CHECK_INTERVAL_MS` | `50000` | 每 50 秒检查一次服务端同步请求 | +| `NETWORK_FRAME_INTERVAL` | `300` | 每 300 帧处理一次网络任务 | +| `LOCAL_FACE_PHOTO_DIR` | `/sd/face_photos` | 本地照片缓存目录 | +| `CAMERA_HMIRROR` | `1` | 水平镜像 | +| `CAMERA_VFLIP` | `1` | 垂直翻转 | + +## 4. 硬件连接 + +| 功能 | K210 引脚 | 说明 | +| --- | --- | --- | +| 发给 STM32 | 24 | UART1 TX | +| 收 STM32 | 25 | UART1 RX | +| 发给 ESP8266 | 34 | UART2 TX | +| 收 ESP8266 | 35 | UART2 RX | +| 蓝/绿/红 LED | 12 / 13 / 14 | 状态提示 | +| 按键 | 16 | 添加、删除、删除全部 | + +UART 参数: + +```text +115200, 8 data bits, no parity, 1 stop bit +``` + +## 5. SD 卡文件 + +| 文件或目录 | 作用 | +| --- | --- | +| `/sd/recordftr.txt` | 人脸特征库,每行一个人脸 | +| `/sd/face_mapping.txt` | face_id 到学号的本地映射 | +| `/sd/sync_version.txt` | K210 已同步到的服务端版本号 | +| `/sd/face_photos/` | 新增人脸时保存的本地照片 | +| `/sd/FaceDetection.smodel` | 人脸检测模型 | +| `/sd/FaceLandmarkDetection.smodel` | 人脸关键点模型 | +| `/sd/FeatureExtraction.smodel` | 人脸特征提取模型 | + +`recordftr.txt` 的一行大致是: + +```text +ID:1#[0.0123, -0.0456, ...] +``` + +或同步映射后: + +```text +20240001#[0.0123, -0.0456, ...] +``` + +代码中已经不再用 `eval` 解析特征,而是只解析安全的浮点数组文本。 + +## 6. 启动流程 + +K210 启动后主要执行: + +1. 设置堆内存到 1MB。 +2. 初始化 UART1 和 UART2。 +3. 初始化 K210 侧 ESP8266,尝试连接 WiFi。 +4. 加载本地映射 `/sd/face_mapping.txt`。 +5. 加载本地同步版本 `/sd/sync_version.txt`。 +6. 加载三个 KPU 模型。 +7. 初始化 LCD 和摄像头。 +8. 初始化按键中断。 +9. 加载本地人脸特征库 `/sd/recordftr.txt`。 +10. 扫描本地照片目录,把未上传照片加入队列。 +11. 如果 WiFi 可用,执行一次统一同步。 +12. 进入主循环。 + +## 7. 人脸识别流程 + +每一帧画面大致按下面处理: + +1. 摄像头采集图像。 +2. `task_fd` 做人脸检测。 +3. `task_ld` 做关键点定位。 +4. 根据关键点做仿射变换,把人脸对齐到 128x128。 +5. `task_fe` 提取人脸特征。 +6. 与 `peoples` 中的本地特征逐个比对。 +7. 如果最高分大于 `85`,认为识别成功。 +8. 如果识别成功,LCD 画绿色框;陌生人画红色框。 +9. 每隔 `SEND_INTERVAL_MS = 1000` 毫秒,向 STM32 发送一次状态变化。 + +发给 STM32 的格式: + +```text +ID:<学号或face_id>,NAME:<显示名>,CONF:<置信度> +``` + +示例: + +```text +ID:20240001,NAME:20240001,CONF:0.95 +``` + +如果陌生人: + +```text +UNKNOWN +``` + +## 8. 按键操作 + +| 操作 | 程序状态 | 作用 | +| --- | --- | --- | +| 短按 | 正常识别界面 | 进入添加人脸模式 | +| 长按约 2 秒 | 正常识别界面 | 进入删除人脸模式 | +| 超长按约 5 秒 | 正常识别界面 | 进入删除全部人脸模式 | +| 添加模式中短按 | 检测到陌生人脸时 | 确认添加 | +| 添加模式中长按 | 添加模式 | 退出添加模式 | +| 删除模式中短按 | 删除模式 | 切换候选项 | +| 删除模式中长按 | 删除模式 | 确认删除当前项 | + +## 9. 添加人脸流程 + +1. 正常识别界面短按按键。 +2. LCD 显示 `Add Face Mode`。 +3. 摄像头检测到陌生人脸后显示 `Ready`。 +4. 再短按确认添加。 +5. K210 分配一个未使用的 `face_id`,例如 `1`。 +6. 特征写入 `/sd/recordftr.txt`,名称为 `ID:1`。 +7. 当前画面压缩为 JPEG。 +8. JPEG 保存到 `/sd/face_photos/1.jpg`。 +9. `1` 加入照片上传队列。 +10. 后续网络任务会上传到服务端。 + +服务端接收接口: + +```text +POST /api/v1/faces/photos/1 +``` + +## 10. 人脸映射同步 + +新增人脸后,K210 只知道它是 `ID:1`,不知道真实学号。管理员需要在 Qt 管理端把 `1` 映射成学号: + +```json +{ + "mappings": { + "1": "20240001" + } +} +``` + +K210 同步后会: + +1. 拉取服务端 `face_mappings`。 +2. 修改本地 `face_mapping.txt`。 +3. 修改 `recordftr.txt` 中对应人脸的名称。 +4. 后续发送给 STM32 的 ID 就会变成真实学号。 + +## 11. 照片上传队列 + +K210 不会在识别每一帧时都做网络请求,因为网络请求会阻塞画面。它采用“分时网络任务”: + +1. 新照片先保存到 SD 卡。 +2. 只把 face_id 放入 `photo_upload_queue`。 +3. 每隔一定帧数,主循环处理一个网络任务。 +4. 如果上传失败,重新放回队列。 + +这样可以减少卡顿,并且即使网络临时不可用,照片也不会丢,因为原始 JPEG 还在 SD 卡。 + +## 12. 服务端同步 + +K210 主要使用统一同步接口: + +```text +GET /api/v1/sync/state?client=k210&last_version=<本地版本> +``` + +如果有变化,服务端返回 K210 需要的数据: + +```json +{ + "version": 5, + "changed": true, + "state": { + "face_mappings": { + "1": "20240001" + }, + "pending_delete": ["1"], + "delete_tasks": [ + { + "task_id": "delete-1", + "face_id": "1" + } + ] + } +} +``` + +K210 会先处理删除任务,再应用映射。删除成功后发送 ACK: + +```json +{ + "client": "k210", + "version": 5, + "deleted": ["1"], + "task_ids": ["delete-1"] +} +``` + +如果 ACK 失败,K210 不会推进本地版本号,下一次还会重试。 + +## 13. 删除人脸 + +K210 本地删除有两种来源: + +### 13.1 K210 按键本地删除 + +这只会删除 K210 本地 `recordftr.txt` 和本地照片,不会自动删除服务端记录。适合现场维护。 + +### 13.2 Qt 管理端远程删除 + +Qt 删除会在服务端创建待删除任务。K210 同步后删除本地人脸,并向服务端确认。适合后台统一管理。 + +## 14. K210 常用接口 + +| 方法 | 路径 | 用途 | +| --- | --- | --- | +| `POST` | `/api/v1/faces/photos/` | 上传 JPEG 照片 | +| `GET` | `/api/v1/faces/photos` | 查询服务端已有照片 | +| `GET` | `/api/v1/faces/mappings` | 拉取人脸 ID 到学号映射 | +| `GET` | `/api/v1/faces/pending-delete` | 拉取待删除列表 | +| `POST` | `/api/v1/faces/delete-ack` | 旧式删除确认 | +| `GET` | `/api/v1/sync/state?client=k210&last_version=N` | 推荐统一同步 | +| `POST` | `/api/v1/sync/ack` | 推荐统一同步确认 | +| `GET` | `/api/v1/sync/request` | 查询 Qt/服务端是否请求同步 | +| `POST` | `/api/v1/sync/request/ack` | 确认收到同步请求 | + +## 15. 常见问题 + +| 现象 | 可能原因 | 处理 | +| --- | --- | --- | +| 模型加载失败 | SD 卡缺少 `.smodel` 文件 | 把三个模型放到 `/sd/` 根目录 | +| WiFi 连接失败 | SSID/密码错误、ESP8266 未启动、波特率不匹配 | 看串口日志,代码会尝试多个波特率 | +| 添加人脸失败 | 没检测到陌生人脸、SD 卡不可写、照片保存失败 | 检查光照、SD 卡和日志 | +| Qt 看不到照片 | 上传队列未完成、服务端地址不一致、Token 错误 | 看 K210 `[PHOTO]`、`[NET]`、`[ESP]` 日志 | +| 删除后仍识别 | K210 尚未同步或 ACK 失败 | 等待同步,检查 `/api/v1/sync/state` | +| 发给 STM32 的还是 `ID:1` | 映射没有保存或 K210 未同步 | Qt 保存映射后触发同步 | + +## 16. 修改配置时要注意 + +1. K210 的服务端 IP、端口、Token 必须和服务端一致。 +2. K210 的 WiFi 配置和 STM32 可以相同,也可以不同,但都要能访问服务端。 +3. K210 侧照片上传使用自己的 ESP8266,不依赖 STM32 的 ESP8266。 +4. K210 本地 SD 卡数据很重要,删除或格式化 SD 卡会丢失本地人脸特征。 +5. 如果重新录入人脸,原 face_id 可能变化,需要重新在 Qt 管理端设置映射。 diff --git a/docs/03_服务端文档.md b/docs/03_服务端文档.md index d14465f..286babd 100644 --- a/docs/03_服务端文档.md +++ b/docs/03_服务端文档.md @@ -1,152 +1,657 @@ # 服务端文档 -## 1. 推荐服务端版本 -当前项目推荐使用:`server/server_encrypted.cpp` +## 1. 服务端的作用 -原因: -- 支持 STM32 上传加密签到数据。 -- 支持 Qt 管理端所需管理 API。 -- 支持 K210 人脸照片与映射同步相关 API。 +服务端是整个系统的数据中心。STM32、K210 和 Qt 管理端都通过 HTTP 与它通信。 -## 2. 端口与数据文件 -- HTTP 端口:`8082` -- SQLite 数据库:`attendance.db` -- 启动备份目录:`backups/` -- 日志:运行时生成 `attendance.log` -- 旧数据迁移来源:`attendance_database.csv`、`timeslots_config.json`、`mapping_config.json`、`face_id_mapping.json`、`pending_delete.json`、`sync_request.json`、`sync_version.txt` -- 人脸目录:`faces/` -- 全局同步版本:存储在 `attendance.db` 的 `kv_store` 表中。 -- 删除任务:存储在 `pending_delete` 表中,包含 `task_id`、`face_id`、`student_id`、`created_at`、`status`、`ack_at`。 -- 设备同步状态:存储在 `device_sync_state` 表中,用于追踪各端最近同步版本和 ACK 版本。 +它负责: -## 3. 签到数据链路 -1. STM32 调用 `POST /api/v1/attendance/checkins` 上传 Base64 密文。 -2. 服务端按 AES-128-CBC 解密并解析 JSON。 -3. 将记录写入 `attendance.db` 的 `records` 表(含 `class_id` 字段)。 +1. 接收 STM32 加密上传的签到记录。 +2. 解密、校验并保存记录。 +3. 提供考勤记录和统计数据给 Qt。 +4. 保存签到时间段配置。 +5. 保存学生姓名映射和班级名称映射。 +6. 保存 K210 上传的人脸照片。 +7. 保存人脸 ID 到真实学号的映射。 +8. 维护待删除人脸任务。 +9. 维护全局同步版本号。 +10. 提供健康检查、存储状态和时间接口。 -## 4. 核心 API(按模块) -### 4.1 考勤与统计 -- `POST /api/v1/attendance/checkins` -- `GET /api/v1/attendance/records?limit=&offset=` -- `GET /api/v1/attendance/statistics` -- `POST /api/v1/attendance/records/clear` -- `DELETE /api/v1/attendance/records?timestamp=&student_id=` -- `GET /api/v1/system/health` -- `GET /api/v1/system/storage` -- `GET /api/v1/system/time` +当前推荐服务端文件: -说明: -- `/api/v1/attendance/records` 与 `/api/v1/attendance/statistics` 默认返回明文 JSON;请求头包含 `X-Encrypted-Response: true` 时返回加密数据。 +```text +server/server_encrypted.cpp +``` -### 4.2 时间段与查重 -- `POST /api/v1/config/time-slots` -- `GET /api/v1/config/time-slots` -- `GET /api/v1/attendance/duplicates?student_id=&slot_start=&slot_end=` -- `GET /api/v1/attendance/today?slot_start=&slot_end=` +## 2. 技术栈 -### 4.3 映射与照片 -- `POST /api/v1/config/mappings` -- `GET /api/v1/config/mappings` -- `GET /api/v1/faces/photos` -- `GET /api/v1/faces/photos/` -- `POST /api/v1/faces/photos/` -- `POST /api/v1/faces/mappings` -- `GET /api/v1/faces/mappings` -- `GET /api/v1/faces/student-ids` +| 项 | 内容 | +| --- | --- | +| 语言 | C++ | +| HTTP 实现 | 原生 socket + 每连接一个线程 | +| 数据库 | SQLite | +| 加密库 | OpenSSL EVP AES-128-CBC | +| 图片存储 | 本地 `faces/*.jpg` | +| 默认端口 | `8082` | +| 默认数据库 | `attendance.db` | -### 4.4 统一同步接口(推荐) -- `GET /api/v1/sync/state?client=&last_version=` -- `POST /api/v1/sync/ack` -- `GET /api/v1/sync/version` +## 3. 运行时文件 -说明: -- 服务端是唯一权威状态源。 -- 每次签到、清空/删除记录、修改时间段、修改映射、上传照片、删除人脸,服务端都会递增 `kv_store.sync_version`。 -- 设备端保存自己的 `last_version`,请求时带给服务端。 -- 服务端会记录每个 `client` 的 `last_seen_version`、`last_ack_version` 和 `last_seen_at`,可通过 `/api/v1/system/storage` 查看。 -- 如果版本无变化,服务端返回 `changed:false`,设备端无需重复解析配置。 -- 如果版本有变化,服务端返回 `changed:true` 和当前客户端需要的状态。 -- `/api/v1/sync/version` 只返回当前版本和服务端时间,适合调试时快速确认同步版本。 +服务端启动后主要依赖当前工作目录下的文件。在哪个目录运行,就在哪个目录读写这些文件。 -客户端示例: -- `client=k210`:返回 `face_mappings` 和 `pending_delete`。 -- `client=stm32`:返回 `time_slots` 和 `server_time`。 -- `client=qt`:返回时间段、人脸映射、照片列表、学生ID列表、记录、统计和映射配置。 -- `client=all`:返回全部可同步状态。 +| 文件或目录 | 当前用途 | +| --- | --- | +| `attendance.db` | 主 SQLite 数据库 | +| `faces/` | 人脸照片目录 | +| `backups/` | 数据库备份目录 | +| `attendance_database.csv` | 旧版 CSV 记录,首次初始化时可导入 | +| `timeslots_config.json` | 旧版时间段配置,首次初始化时可导入 | +| `mapping_config.json` | 旧版姓名/班级映射,首次初始化时可导入 | +| `face_id_mapping.json` | 旧版人脸映射,首次初始化时可导入 | +| `pending_delete.json` | 旧版待删除列表,首次初始化时可导入 | +| `sync_request.json` | 旧版同步请求标志,首次初始化时可导入 | +| `sync_version.txt` | 旧版同步版本号,首次初始化时可导入 | -K210 删除确认示例: +首次启动时,如果 SQLite 表中没有数据,服务端会尝试从这些旧文件导入;导入后主要读写 SQLite。 + +## 4. SQLite 表结构 + +服务端启动时会创建这些表: + +### 4.1 `records` + +保存考勤记录。 + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `id` | INTEGER PRIMARY KEY AUTOINCREMENT | 自增主键 | +| `timestamp` | TEXT | 签到时间 | +| `student_id` | TEXT | 学号 | +| `name` | TEXT | 名称 | +| `confidence` | TEXT | 置信度 | +| `status` | TEXT | 状态,例如 `success` | +| `class_id` | TEXT | 班级编号 | + +索引: + +```sql +idx_records_student_time(student_id,timestamp) +idx_records_timestamp(timestamp) +``` + +### 4.2 `kv_store` + +保存配置型数据。 + +| key | value 内容 | +| --- | --- | +| `time_slots` | 时间段 JSON | +| `mapping_config` | 学生姓名和班级名称映射 | +| `sync_request` | K210 同步请求标志 | +| `sync_version` | 全局同步版本号 | + +### 4.3 `face_mappings` + +保存 K210 原始 face_id 到真实学号的映射。 + +| 字段 | 含义 | +| --- | --- | +| `face_id` | K210 本地人脸编号 | +| `student_id` | 真实学号 | + +### 4.4 `pending_delete` + +保存等待 K210 删除的任务。 + +| 字段 | 含义 | +| --- | --- | +| `task_id` | 删除任务 ID | +| `face_id` | K210 需要删除的人脸 ID | +| `student_id` | 对应学生 ID | +| `created_at` | 创建时间 | +| `status` | 状态,默认 `pending` | +| `ack_at` | 确认时间 | + +### 4.5 `device_sync_state` + +保存各客户端同步状态。 + +| 字段 | 含义 | +| --- | --- | +| `client` | 客户端名,例如 `qt`、`stm32`、`k210` | +| `last_seen_version` | 客户端最近看到的服务端版本 | +| `last_ack_version` | 客户端最近确认的版本 | +| `last_seen_at` | 最近访问时间 | + +## 5. 默认配置 + +| 配置 | 当前值 | 说明 | +| --- | --- | --- | +| `HTTP_PORT` | `8082` | HTTP 监听端口 | +| `ATTENDANCE_BIND_HOST` | `0.0.0.0` | 默认监听地址,可用环境变量覆盖 | +| `DEFAULT_API_TOKEN` | `atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV` | 默认 API Token | +| `DEFAULT_CORS_ORIGIN` | `http://localhost` | 默认 CORS 来源 | +| `MAX_JSON_BODY_SIZE` | `64 KB` | 普通 JSON 请求体上限 | +| `MAX_PHOTO_BODY_SIZE` | `512 KB` | 照片上传请求体上限 | +| `DEFAULT_TIMESLOTS_JSON` | `{"time_slots":[]}` | 默认时间段为空 | + +可以通过环境变量覆盖: + +```powershell +$env:ATTENDANCE_BIND_HOST="0.0.0.0" +$env:ATTENDANCE_API_TOKEN="你的Token" +$env:ATTENDANCE_CORS_ORIGIN="http://localhost" +``` + +## 6. 鉴权规则 + +服务端对大多数接口要求请求头: + +```text +X-Api-Token: +``` + +允许不带 Token 的情况: + +1. `OPTIONS` 预检请求。 +2. `GET /api/health`。 + +新接口和旧接口都使用同样的鉴权逻辑。 + +## 7. 路由兼容关系 + +服务端内部仍有一些旧路径,例如 `/api/records`、`/api/timeslots`。为了让接口更清晰,代码提供了 `/api/v1/...` 新路径,并在服务端内部转换到旧路径。 + +| 新路径 | 内部兼容路径 | 说明 | +| --- | --- | --- | +| `/api/v1/system/health` | `/api/health` | 健康检查 | +| `/api/v1/system/time` | `/api/time` | 获取服务器时间 | +| `/api/v1/attendance/checkins` | `/api/checkin` | 加密签到上传 | +| `/api/v1/attendance/records` | `/api/records` | 获取/删除考勤记录 | +| `/api/v1/attendance/records/clear` | `/api/clear` | 清空记录 | +| `/api/v1/attendance/statistics` | `/api/statistics` | 获取统计 | +| `/api/v1/attendance/duplicates` | `/api/check_duplicate` | 查重 | +| `/api/v1/attendance/today` | `/api/today_records` | 今日某时间段记录 | +| `/api/v1/config/time-slots` | `/api/timeslots` | 时间段 | +| `/api/v1/config/mappings` | `/api/mapping_config` | 学生/班级映射 | +| `/api/v1/faces/mappings` | `/api/face_id_mapping` | 人脸映射 | +| `/api/v1/faces/photos` | `/api/face_photos` | 照片列表 | +| `/api/v1/faces/photos/` | `/api/face_photo/` 或 `/api/face_photo_simple/` | 下载/上传照片 | +| `/api/v1/faces/student-ids` | `/api/all_student_ids` | 全部学生 ID | +| `/api/v1/faces/pending-delete` | `/api/pending_delete` | 待删除任务 | +| `/api/v1/faces/delete-ack` | `/api/confirm_delete` | 旧式删除确认 | +| `/api/v1/faces/` | `/api/face/` | 删除人脸 | +| `/api/v1/sync/request` | `/api/sync_request` | 同步请求标志 | +| `/api/v1/sync/request/ack` | `/api/sync_ack` | 同步请求确认 | +| `/api/v1/sync/trigger` | `/api/trigger_sync` | 触发同步 | + +文档推荐使用 `/api/v1/...`,旧路径主要为了兼容旧代码。 + +## 8. 核心 API + +以下示例默认服务端地址是: + +```text +http://127.0.0.1:8082 +``` + +### 8.1 健康检查 + +```text +GET /api/v1/system/health +``` + +返回示例: + +```json +{ + "status": "healthy", + "time": "2026-05-01 10:00:00", + "storage": "sqlite", + "database": "attendance.db", + "database_ok": true, + "quick_check": "ok", + "sync_version": 1 +} +``` + +### 8.2 存储状态 + +```text +GET /api/v1/system/storage +``` + +用于检查 SQLite、记录数量、照片数量、待删除任务数量等。 + +### 8.3 获取服务器时间 + +```text +GET /api/v1/system/time +``` + +返回: + +```json +{ + "epoch": 1777600800, + "server_time": "2026-05-01 10:00:00" +} +``` + +STM32 使用 `epoch` 转成 UTC+8 时间。 + +### 8.4 加密签到上传 + +```text +POST /api/v1/attendance/checkins +X-Encrypted: AES128-CBC +X-Api-Token: +``` + +请求体是 Base64 字符串。解密后的 JSON: + +```json +{ + "student_id": "20240001", + "name": "张三", + "confidence": "0.95", + "status": "success", + "class_id": "1" +} +``` + +字段校验: + +| 字段 | 要求 | +| --- | --- | +| `student_id` | 必填,安全 ID,长度不超过 32 | +| `name` | 可选,长度不超过 64 | +| `confidence` | 必须是 0.0 到 1.0 | +| `status` | 只能是 `success`、`failed`、`late` | +| `class_id` | 可选,安全 ID,长度不超过 32,默认 `0` | + +成功返回: + +```json +{ + "success": true, + "message": "签到成功", + "timestamp": "2026-05-01 10:00:00" +} +``` + +### 8.5 获取考勤记录 + +```text +GET /api/v1/attendance/records?limit=1000&offset=0 +``` + +`limit` 范围是 1 到 5000,`offset` 范围是 0 到 1000000。 + +返回数组: + +```json +[ + { + "timestamp": "2026-05-01 08:10:00", + "student_id": "20240001", + "name": "张三", + "confidence": 0.95, + "status": "success", + "class_id": "1" + } +] +``` + +如果请求头带: + +```text +X-Encrypted-Response: true +``` + +服务端会把响应 JSON 加密后以 Base64 返回。Qt 当前普通记录请求没有启用这个请求头。 + +### 8.6 获取统计 + +```text +GET /api/v1/attendance/statistics +``` + +返回: + +```json +{ + "total": 120, + "today": 8, + "server_time": "2026-05-01 10:00:00" +} +``` + +### 8.7 清空记录 + +```text +POST /api/v1/attendance/records/clear +``` + +成功后会删除 `records` 表中的所有记录,并提升同步版本。 + +### 8.8 删除单条记录 + +```text +DELETE /api/v1/attendance/records?timestamp=2026-05-01%2008%3A10%3A00&student_id=20240001 +``` + +成功后提升同步版本。 + +### 8.9 时间段配置 + +获取: + +```text +GET /api/v1/config/time-slots +``` + +保存: + +```text +POST /api/v1/config/time-slots +Content-Type: application/json +``` + +请求体: + +```json +{ + "time_slots": [ + { + "name": "上午签到", + "start_time": "08:00", + "end_time": "09:30", + "enabled": true + } + ] +} +``` + +当前默认值是: + +```json +{ + "time_slots": [] +} +``` + +服务端还包含旧默认四时段的迁移判断:如果数据库里保存的是旧的四个默认时段,会自动改为空数组。 + +### 8.10 重复签到检查 + +```text +GET /api/v1/attendance/duplicates?student_id=20240001&slot_start=08:00&slot_end=09:30 +``` + +用于 STM32 在线时补充查重。 + +### 8.11 今日某时段已签到记录 + +```text +GET /api/v1/attendance/today?slot_start=08:00&slot_end=09:30 +``` + +返回: + +```json +{ + "date": "2026-05-01", + "records": ["20240001", "20240002"] +} +``` + +STM32 开机后用它构建本地今日缓存。 + +### 8.12 学生姓名和班级名称映射 + +```text +GET /api/v1/config/mappings +POST /api/v1/config/mappings +``` + +格式: + +```json +{ + "student_name_map": { + "20240001": "张三" + }, + "class_name_map": { + "1": "软件工程一班" + } +} +``` + +这个映射主要给 Qt 显示使用,不改变 STM32 签到上传的原始学号。 + +### 8.13 人脸照片 + +照片列表: + +```text +GET /api/v1/faces/photos +``` + +返回: + +```json +{ + "photos": [ + { + "id": "1", + "filename": "1.jpg" + } + ] +} +``` + +上传照片: + +```text +POST /api/v1/faces/photos/1 +Content-Type: image/jpeg +``` + +下载照片: + +```text +GET /api/v1/faces/photos/1 +``` + +成功时返回 `image/jpeg`。 + +### 8.14 人脸 ID 映射 + +```text +GET /api/v1/faces/mappings +POST /api/v1/faces/mappings +``` + +格式: + +```json +{ + "mappings": { + "1": "20240001" + } +} +``` + +保存映射后服务端会: + +1. 写入 `face_mappings`。 +2. 尝试按映射重命名照片。 +3. 提升同步版本。 +4. 设置 K210 同步请求标志。 + +### 8.15 获取全部学生 ID + +```text +GET /api/v1/faces/student-ids +``` + +从 `records` 表中提取所有出现过的学生 ID。 + +### 8.16 删除人脸 + +```text +DELETE /api/v1/faces/20240001 +DELETE /api/v1/faces/20240001?delete_records=true +``` + +服务端会: + +1. 删除相关照片。 +2. 删除相关映射。 +3. 如果 `delete_records=true`,删除该学生相关考勤记录。 +4. 创建 K210 待删除任务。 +5. 提升同步版本。 +6. 设置同步请求标志。 + +返回示例: + +```json +{ + "success": true, + "photo_deleted": true, + "mapping_deleted": true, + "records_deleted": 3, + "k210_pending": true, + "sync_requested": true +} +``` + +## 9. 统一同步接口 + +### 9.1 获取版本 + +```text +GET /api/v1/sync/version +``` + +返回: + +```json +{ + "version": 6, + "server_time": "2026-05-01 10:00:00" +} +``` + +### 9.2 获取同步状态 + +```text +GET /api/v1/sync/state?client=&last_version= +``` + +`client` 可取: + +| client | 服务端返回内容 | +| --- | --- | +| `stm32` | `time_slots` | +| `k210` | `face_mappings`、`pending_delete`、`delete_tasks` | +| `qt` | 记录、统计、时间段、映射、照片列表、学生 ID、人脸映射 | +| `all` 或空 | 尽可能返回全部 | + +如果没有变化: + +```json +{ + "version": 6, + "changed": false, + "client": "stm32", + "server_time": "2026-05-01 10:00:00" +} +``` + +如果有变化: + +```json +{ + "version": 7, + "changed": true, + "client": "k210", + "server_time": "2026-05-01 10:00:00", + "state": { + "face_mappings": {}, + "pending_delete": [], + "delete_tasks": [] + } +} +``` + +### 9.3 同步确认 + +```text +POST /api/v1/sync/ack +``` + +请求体: ```json { "client": "k210", - "version": 12, - "deleted": ["2", "3"], - "task_ids": ["2-1770000000", "3-1770000001"] + "version": 7, + "deleted": ["1"], + "task_ids": ["task-1"] } ``` -### 4.5 人脸删除与同步控制 -- `DELETE /api/v1/faces/?delete_records=true` -- `GET /api/v1/faces/pending-delete` -- `POST /api/v1/faces/delete-ack` -- `GET /api/v1/sync/request` -- `POST /api/v1/sync/request/ack` -- `POST /api/v1/sync/trigger` +用途: -说明:这些是 v1 下的兼容控制接口;推荐设备端优先使用统一同步接口,只有手动调试或兼容旧流程时才单独调用。 +1. 记录客户端已经确认的版本。 +2. 移除已经完成的删除任务。 +3. 必要时再次提升同步版本。 -### 4.6 旧接口兼容 -服务端内部仍保留旧 `/api/...` 路由兼容层,便于历史程序临时访问;新代码和新文档统一使用 `/api/v1/...`。 +## 10. 编译与运行 -## 5. 编译与运行 -Windows(示例): -```bash -cd server +### 10.1 Windows MinGW 示例 + +如果已经装好 MinGW、OpenSSL、SQLite: + +```powershell g++ -o server_encrypted.exe server_encrypted.cpp -lws2_32 -lcrypto -lssl -lsqlite3 -std=c++11 -server_encrypted.exe +.\server_encrypted.exe ``` -Linux(示例): +### 10.2 Linux 示例 + ```bash -cd server -g++ -o server_encrypted server_encrypted.cpp -lcrypto -lssl -lsqlite3 -lpthread -std=c++11 +g++ -o server_encrypted server_encrypted.cpp -lcrypto -lssl -lsqlite3 -std=c++11 -pthread ./server_encrypted ``` -说明:服务端启动时会自动创建 `attendance.db`。如果检测到旧 CSV/JSON 文件,会在首次启动时导入;之后以后端 API 和 SQLite 为准。 +### 10.3 运行目录建议 -## 6. 备份与恢复 -服务端启动时如果发现已有 `attendance.db`,会先复制到 `backups/attendance_YYYY-MM-DD_HH-MM-SS.db`。备份失败时服务端会停止启动,避免在没有恢复点的情况下继续写入。 - -恢复步骤: -1. 停止 `server_encrypted`。 -2. 将当前 `attendance.db` 改名保留,例如 `attendance.broken.db`。 -3. 从 `backups/` 选择最近可用备份,复制为 `attendance.db`。 -4. 重新启动服务端,访问 `/api/v1/system/storage`,确认 `ok:true`、`quick_check:"ok"`。 - -## 7. 公网部署安全 -本机或内网演示可以直接监听 `0.0.0.0:8082`。公网部署建议: -- 设置 `ATTENDANCE_BIND_HOST=127.0.0.1`,只让服务端监听本机。 -- 使用 Nginx 或 Caddy 终止 HTTPS,并由反向代理转发到 `127.0.0.1:8082`。 -- 配置 HTTPS 后立即轮换 `ATTENDANCE_API_TOKEN`、设备端 Token 和相关密钥。 -- 反向代理层继续限制来源 IP、请求体大小和访问日志脱敏。 - -## 8. 自动化回归 -服务端启动后可运行: +建议在 `server/` 目录下运行,这样数据库、照片目录和备份目录都会生成在 `server/` 下面: ```powershell -powershell -ExecutionPolicy Bypass -File .\regression_api_tests.ps1 -BaseUrl "http://127.0.0.1:8082" -ApiToken "change-me-token" +cd D:\University\graduation\code\Reconstruct_Qt\server +.\server_encrypted.exe ``` -脚本覆盖 health、storage、time、sync、records、statistics、time-slots、mappings、faces 等低风险接口;真实 AES 签到正文和真实照片上传仍保留手动验证。 +## 11. 安全注意事项 -## 9. 版本说明 -仓库仅保留当前推荐版本:`server_encrypted.cpp`。旧版实验服务端与测试脚本已清理,避免误部署非鉴权接口。 +1. 默认 Token 已写在代码里,不适合直接暴露公网。 +2. 当前是 HTTP 明文传输,公网部署建议放到 HTTPS 反向代理后面。 +3. AES Key/IV 是固定值,演示可以,生产环境不建议固定写死。 +4. SQLite 数据库和照片目录需要定期备份。 +5. `ATTENDANCE_CORS_ORIGIN` 不建议设置成任意来源。 +6. 服务器运行目录要限制写权限,避免误删数据。 -## 10. 关键一致性要求 -- AES Key/IV 必须与 STM32、Qt 完全一致。 -- 若 Qt 无法解析记录,先检查是否误连到非加密版本服务端。 - -## 11. 后续优化 -服务端当前已经完成 SQLite 主存储、v1 接口、自动备份和统一同步模型。后续需要继续跟踪的风险和优化项已统一记录在 `docs/05_项目审查问题清单.md`。 +## 12. 常见问题 +| 现象 | 可能原因 | 处理 | +| --- | --- | --- | +| 401 Unauthorized | 没带 `X-Api-Token` 或 Token 不一致 | 检查四端 Token | +| Qt 连接失败 | 服务端没启动、地址错、防火墙拦截 | 访问 `/api/v1/system/health` | +| STM32 解密失败 | AES Key/IV 不一致或请求体不是 Base64 密文 | 对照三端 AES 参数 | +| 时间段总是旧数据 | SQLite 中已有旧配置 | 当前代码会清理旧默认四时段;自定义旧配置需手动保存为空 | +| K210 删除不同步 | K210 未拉取同步状态或 ACK 失败 | 看 `/api/v1/sync/state?client=k210&last_version=0` | +| 照片上传失败 | 图片超过 512KB、face_id 非法、目录不可写 | 检查服务端日志和 `faces/` 权限 | diff --git a/docs/04_Qt管理端文档.md b/docs/04_Qt管理端文档.md index 4df0482..e8c3cd8 100644 --- a/docs/04_Qt管理端文档.md +++ b/docs/04_Qt管理端文档.md @@ -1,150 +1,200 @@ -# Qt管理端当前功能文档 +# Qt 管理端文档 -## 1. 文档目的 +## 1. Qt 管理端是什么 -本文档用于说明 `QT/` 管理端重构后的实际架构、当前已经实现的功能、服务端接口依赖、构建方式和已知限制。 +Qt 管理端是整个系统的图形化后台。它支持 Windows 桌面端和 Android 端,主要用于: -本文结论以当前源码为准,不再沿用旧版 Widgets 管理端中 `MainWindow`、`NetworkApiManager`、多个独立 `Dialog` 的描述。 +1. 查看考勤记录。 +2. 按班级、学号、姓名、日期筛选记录。 +3. 导出记录 CSV。 +4. 查看统计分析。 +5. 导出统计 CSV。 +6. 配置服务端地址。 +7. 配置签到时间段。 +8. 配置学号到姓名、班级编号到班级名的显示映射。 +9. 查看 K210 上传的人脸照片。 +10. 给 K210 face_id 绑定真实学号。 +11. 删除人脸信息,并可选择删除相关考勤记录。 -## 2. 模块定位 +## 2. 相关文件 -Qt 管理端是人脸识别考勤系统的跨平台管理客户端,主要负责: +| 文件 | 作用 | +| --- | --- | +| `QT/main.cpp` | Qt 程序入口,设置应用信息、图标、QML 引擎 | +| `QT/qml/Main.qml` | 主界面,包括所有页面和对话框 | +| `QT/src/AppController.*` | QML 与 C++ 业务逻辑的中枢 | +| `QT/src/ApiClient.*` | HTTP API 客户端 | +| `QT/src/AttendanceTypes.h` | 考勤记录、统计、时间段结构体 | +| `QT/src/CryptoUtils.*` | AES 解密工具 | +| `QT/src/RecordListModel.*` | 考勤记录模型 | +| `QT/src/StatisticsListModel.*` | 统计模型 | +| `QT/src/TimeSlotListModel.*` | 时间段模型 | +| `QT/src/MappingListModel.*` | 姓名/班级映射模型 | +| `QT/src/FaceListModel.*` | 人脸管理模型 | +| `QT/CMakeLists.txt` | Qt/CMake 构建配置 | +| `QT/android/` | Android Gradle 配置和图标 | +| `QT/assets/` | 桌面端图标 | -- 从服务端拉取考勤记录、统计信息和配置数据。 -- 查看、筛选、导出考勤记录。 -- 按学生维度生成本地统计报表。 -- 管理服务端地址、签到时间段、学生姓名映射、班级名称映射。 -- 管理 K210 上传的人脸照片和人脸 ID 到学号的绑定关系。 -- 在服务端不可达时给出简短状态提示,并自动重连。 +## 3. 技术栈 -当前 Qt 端已经重构为 Qt 6 Quick/QML 架构,主要源码位置如下: +| 分类 | 内容 | +| --- | --- | +| 语言 | C++17、QML | +| UI | Qt Quick、Qt Quick Controls 2 | +| Qt 模块 | Core、Gui、Qml、Quick、QuickControls2、Network | +| 构建系统 | CMake 3.16+ | +| 样式 | `QQuickStyle::setStyle("Basic")` | +| Android 文字渲染 | `NativeTextRendering` | +| 本地配置 | `QSettings`、`QStandardPaths::AppDataLocation` | -| 模块 | 路径 | 说明 | -| --- | --- | --- | -| 程序入口 | `QT/main.cpp` | 初始化应用信息、图标、QML 引擎和 `AppController` | -| 主界面 | `QT/qml/Main.qml` | 单窗口 QML 页面,包含记录、统计、配置、人脸、关于五个页面 | -| 业务中枢 | `QT/src/AppController.*` | 连接 QML 和业务逻辑,负责筛选、统计、配置保存、重连调度 | -| 网络接口 | `QT/src/ApiClient.*` | 封装 HTTP API、请求超时、加密响应解密和 JSON 解析 | -| 数据结构 | `QT/src/AttendanceTypes.h` | 考勤记录、统计项、签到时段等结构定义 | -| 列表模型 | `QT/src/*ListModel.*` | 向 QML 暴露记录、统计、时间段、映射、人脸列表 | -| 加密工具 | `QT/src/CryptoUtils.*` | AES-128-CBC Base64 响应解密 | -| AES 实现 | `QT/src/crypto/tiny_aes.hpp` | AES 底层实现 | -| 构建配置 | `QT/CMakeLists.txt` | Qt 6 CMake 构建、资源、图标、Android 配置 | -| 平台资源 | `QT/assets/`、`QT/android/`、`QT/cmake/` | 桌面图标、Android 图标与平台模板 | +## 4. 应用基础信息 -## 3. 技术栈与界面结构 +位置:`QT/main.cpp` -当前客户端使用: +| 项 | 值 | +| --- | --- | +| ApplicationName | `人脸识别考勤系统` | +| ApplicationVersion | `3.0.0` | +| OrganizationName | `STM32 Face Recognition` | +| 主 QML | `qrc:/qml/Main.qml` | +| 默认窗口大小 | `1560 x 920` | +| PC 最小窗口 | `900 x 620` | +| Android 最小窗口 | `360 x 560` | -- C++17 -- Qt 6.2+ -- Qt Quick / Qt Quick Controls 2 -- Qt Core、Gui、Qml、Quick、QuickControls2、Network -- CMake 3.16+ +## 5. 页面结构 -界面由 `ApplicationWindow` 承载,内部使用 `StackLayout` 切换五个页面: +Qt 管理端是单窗口应用,共 5 个页面: -- 考勤记录 -- 统计分析 -- 配置中心 -- 人脸管理 -- 关于 +| 页面 | 作用 | +| --- | --- | +| 考勤记录 | 查看、筛选、导出、清空记录 | +| 统计分析 | 按时间范围统计学生签到情况 | +| 配置中心 | 服务端地址、签到时间段、名称映射 | +| 人脸管理 | 查看照片、绑定学号、删除人脸 | +| 关于 | 系统说明、版本和作者信息 | -QML 不直接访问网络。所有页面操作统一调用 `AppController` 暴露的 `Q_INVOKABLE` 方法和 `Q_PROPERTY` 状态,数据表格通过 `RecordListModel`、`StatisticsListModel`、`TimeSlotListModel`、`MappingListModel`、`FaceListModel` 渲染。 +PC 宽屏显示左侧导航栏;窄屏和 Android 显示顶部/底部更紧凑的导航布局。 -## 4. 当前已实现功能 +## 6. 网络访问规则 -### 4.1 启动与服务连接 - -应用启动流程: - -1. `main.cpp` 创建 `QGuiApplication`。 -2. 设置应用名称、版本、组织名和窗口图标。 -3. Android 平台启用原生文字渲染,改善中文粗体显示。 -4. 创建 `AppController`。 -5. 将 `appController` 注入 QML 上下文。 -6. 加载 `qrc:/qml/Main.qml`。 - -`AppController` 构造时会: - -- 读取本地服务端地址、映射配置和时间段配置。 -- 设置 `ApiClient` 的服务端地址。 -- 启动健康检查。 -- 如果服务端可用,同步记录、统计、时间段和映射配置。 -- 如果服务端不可用,进入自动重连流程。 - -默认服务端地址: +Qt 端的服务端地址默认是: ```text http://127.0.0.1:8082 ``` -用户可以在“配置中心”修改服务端地址。保存后会写入 `QSettings` 的 `server/url`,下次启动会读取并使用。 +用户可在“配置中心”修改服务端地址。保存后写入本地 `QSettings`: -### 4.2 自动重连与状态提示 +```text +server/url +``` -当前实现了基于 `/api/v1/system/health` 的连接检查: +`ApiClient::buildUrl` 会在地址后自动拼接 `/api`,例如: -- 网络请求超时时间为 5 秒。 -- 服务端不可达时显示简短提示:`服务器未连接,稍后重试`。 -- 自动重连间隔为 5 秒。 -- 重连期间显示:`正在重连服务器`。 -- 服务恢复后自动刷新记录、统计、时间段、映射和人脸相关数据。 +```text +服务端地址: http://127.0.0.1:8082 +传入 endpoint: /v1/system/health +实际请求: http://127.0.0.1:8082/api/v1/system/health +``` -底部 Toast 会展示状态信息,避免把底层网络错误、URL 和长异常文本直接暴露给用户。 +所有请求都会带: -### 4.3 考勤记录查看 +```text +X-Api-Token: +Accept: application/json +``` -“考勤记录”页用于查看服务端同步下来的签到记录。 +Token 默认值写在 `QT/src/ApiClient.cpp`,也可以通过环境变量覆盖: -当前实现: +```powershell +$env:ATTENDANCE_API_TOKEN="你的Token" +``` -- 默认请求最新 `1000` 条记录。 -- 顶部显示总记录数、今日签到数、筛选结果数。 -- 记录按签到时间倒序显示。 -- 支持刷新记录。 -- 支持清空服务端全部考勤记录。 -- 支持导出当前筛选结果为 CSV。 +网络超时: -记录表格字段: +```text +5000 ms +``` -- 签到时间 -- 班级 -- 学号 -- 姓名 -- 置信度 -- 状态 +## 7. 启动流程 -显示规则: +Qt 启动后: -- 班级优先使用“班级编号 -> 班级名称”映射;未配置时显示原班级编号。 -- 姓名优先使用“学号 -> 姓名”映射;未配置时回退到服务端记录中的原始姓名。 -- 状态会被归一化判断是否成功,成功和失败在界面上使用不同样式显示。 +1. `main.cpp` 创建 `QGuiApplication`。 +2. 设置应用图标、应用名、版本号、组织名。 +3. Android 下启用原生文字渲染。 +4. 创建 `AppController`。 +5. `AppController` 读取本地服务端地址、映射配置、时间段配置。 +6. `ApiClient` 设置当前服务端地址。 +7. 启动自动重连状态并执行健康检查。 +8. QML 载入 `Main.qml`。 +9. QML 通过上下文对象 `appController` 调用 C++。 -### 4.4 记录筛选 +## 8. 自动重连 -记录页支持本地筛选。筛选条件包括: +如果健康检查失败,`AppController` 会进入重连状态。 -- 班级 -- 学号 -- 姓名关键字 -- 日期范围 +相关参数: -日期范围支持: +```text +ReconnectIntervalMs = 5000 +``` -- 全部时间 -- 今天 -- 最近 7 天 -- 最近 30 天 -- 指定日期 +重连成功后,如果之前处于重连中,会调用: -筛选基于当前已经加载到客户端内存中的记录执行,不会为每次筛选重新请求服务端。 +1. `refreshAll()` +2. `refreshFaces()` -### 4.5 记录导出与清空 +这样服务端恢复后,记录、配置、照片和映射会重新同步。 -当前筛选结果可以导出为 CSV 文件。 +## 9. 考勤记录页 -导出内容包括: +功能: + +1. 显示服务端记录。 +2. 显示总记录数、今日记录数、当前筛选结果数。 +3. 按班级筛选。 +4. 按学号筛选。 +5. 按姓名关键字筛选。 +6. 按日期范围筛选。 +7. 导出当前筛选结果。 +8. 清空服务端所有记录。 + +记录加载默认请求: + +```text +GET /api/v1/attendance/records?limit=1000&offset=0 +``` + +注意:当前 Qt 统计和筛选主要基于已加载的记录。如果服务端超过 1000 条,界面统计不一定覆盖全部历史记录。 + +## 10. 筛选规则 + +筛选在 Qt 本地完成。 + +支持: + +| 筛选项 | 说明 | +| --- | --- | +| 班级 | `class_id` 匹配 | +| 学号 | `student_id` 匹配 | +| 姓名 | 映射姓名或原始 name 包含关键字 | +| 日期 | 全部、今天、最近 7 天、最近 30 天、指定日期 | + +姓名显示优先级: + +1. 如果学生映射里有该学号,显示映射姓名。 +2. 否则显示服务端记录中的 `name`。 +3. 如果都没有,则显示原始学号。 + +班级显示优先级: + +1. 如果班级映射里有该 `class_id`,显示映射班级名。 +2. 否则显示 `class_id`。 + +## 11. 记录导出 + +导出 CSV 文件时会写入: ```text # 考勤记录导出 @@ -153,76 +203,85 @@ http://127.0.0.1:8082 签到时间,班级编号,班级名称,学号,姓名,置信度,状态 ``` -“清空记录”会调用服务端 `/api/v1/attendance/records/clear`。界面在执行前会弹出确认对话框,并提示该操作不可恢复。 +字段会做 CSV 转义,包含逗号、双引号、换行时会自动加引号并转义。 -需要注意:当前界面只开放“清空全部记录”,没有在记录表中开放单条删除或批量删除入口。 +## 12. 统计分析页 -### 4.6 统计分析 +统计页基于当前加载记录在本地计算。 -“统计分析”页基于当前加载到客户端的考勤记录,在本地按学生维度计算统计结果。 +支持时间范围: -支持: +1. 今天。 +2. 最近 7 天。 +3. 最近 30 天。 +4. 本月。 +5. 自定义开始和结束日期。 -- 快捷选择统计范围。 -- 手动输入开始日期和结束日期。 -- 显示统计学生数、统计记录数、统计范围。 -- 导出统计报表 CSV。 +统计指标: -统计字段: - -- 学号 -- 姓名 -- 总签到次数 -- 今日签到次数 -- 最后签到时间 -- 平均置信度 +| 指标 | 含义 | +| --- | --- | +| 统计学生数 | 当前范围内出现过的学生数量 | +| 统计记录数 | 当前范围内记录数量 | +| 总签到次数 | 每个学生在范围内的记录数量 | +| 今日签到 | 每个学生今天的记录数量 | +| 最后签到时间 | 每个学生最新一条记录时间 | +| 平均置信度 | 每个学生记录的平均 confidence | 导出字段: ```text +考勤统计报表,<范围> 学号,姓名,总签到次数,今日签到,最后签到时间,平均置信度 ``` -统计结果不是服务端全量统计,而是对当前客户端已经拉取的最多 `1000` 条记录进行本地计算。 +## 13. 配置中心 -### 4.7 配置中心 +配置中心包含三块: -“配置中心”集中管理服务连接、签到时间段和名称映射。 +1. 服务端地址。 +2. 时间段配置。 +3. 名称映射配置。 -#### 服务端地址 +### 13.1 服务端地址 -支持: - -- 查看当前服务端地址。 -- 修改并保存服务端地址。 -- 测试服务端连接。 - -保存后会写入本地 `QSettings`: +字段示例: ```text -server/url +http://27.106.113.205:8082 ``` -#### 签到时间段 +按钮: -支持: +| 按钮 | 作用 | +| --- | --- | +| 保存地址 | 写入 `QSettings`,并立即切换 `ApiClient` 的服务端 | +| 测试连接 | 请求 `/api/v1/system/health` | -- 查看当前时间段。 -- 新增时间段。 -- 编辑时间段。 -- 删除时间段。 -- 恢复默认时间段。 -- 从服务端同步时间段。 -- 保存时间段到本地并上传服务端。 +### 13.2 时间段配置 -默认时间段: +时间段格式: -| 名称 | 开始时间 | 结束时间 | 默认状态 | -| --- | --- | --- | --- | -| 上午签到 | 08:00 | 09:30 | 启用 | -| 中午签退 | 12:00 | 13:00 | 启用 | -| 下午签到 | 14:00 | 15:00 | 启用 | -| 晚上签退 | 17:00 | 18:30 | 启用 | +```json +{ + "name": "上午签到", + "start_time": "08:00", + "end_time": "09:30", + "enabled": true +} +``` + +当前默认时间段为空。第一次使用时需要手动新增。 + +按钮: + +| 按钮 | 作用 | +| --- | --- | +| 展开/收起 | 展示或隐藏时间段列表 | +| 新增时段 | 打开新增对话框 | +| 清空时段 | 清空本地时间段并保存同步到服务端 | +| 从服务器同步/同步 | 从服务端读取时间段;服务端为空数组时也会清空本地时间段 | +| 保存时间段/保存 | 保存到本地并上传服务端 | 本地文件: @@ -230,20 +289,25 @@ server/url timeslot_config.json ``` -#### 名称映射 +如果本地文件中刚好是旧版本四个默认时间段,当前代码会自动迁移为空配置。 -支持两类映射: +### 13.3 名称映射配置 -- 学号 -> 姓名 -- 班级编号 -> 班级名称 +学生映射: -支持操作: +```json +{ + "20240001": "张三" +} +``` -- 新增映射。 -- 直接编辑映射键和值。 -- 删除映射。 -- 从服务端加载映射配置。 -- 保存到本地并上传服务端。 +班级映射: + +```json +{ + "1": "软件工程一班" +} +``` 本地文件: @@ -251,378 +315,122 @@ timeslot_config.json mapping_config.json ``` -映射会影响记录列表和统计报表中的姓名、班级显示。 +## 14. 人脸管理页 -### 4.8 人脸管理 +人脸管理页把三类数据合并展示: -“人脸管理”页用于维护 K210 上传的人脸照片和人脸 ID 到学号的绑定关系。 +1. 服务端照片列表。 +2. 服务端已有学生 ID。 +3. 服务端 face_id 到学生 ID 映射。 -当前实现: +如果服务端已经把照片从原始 face_id 文件名重命名为学号文件名,Qt 仍会把已有映射中的原始 face_id 展示出来,保存时保留这类真实映射,并跳过未编辑的“ID 等于学号”的默认占位项。 -- 从服务端同步人脸照片 ID。 -- 从服务端同步全部学生 ID。 -- 从服务端同步人脸 ID 到学号的映射。 -- 合并照片 ID 和学生 ID,生成统一管理列表。 -- 显示照片或“无照片”状态。 -- 编辑人脸 ID 对应的学号。 -- 保存人脸映射到服务端。 -- 删除指定人脸。 -- 删除时可选择同时删除该学生相关考勤记录。 +用户可以: -照片加载方式: +1. 刷新人脸库。 +2. 为每张照片填写真实学号。 +3. 保存映射。 +4. 删除人脸。 +5. 删除人脸时选择是否同时删除该学生的考勤记录。 + +照片下载流程: + +1. Qt 获取照片 ID 列表。 +2. 对每个 ID 请求 `/api/v1/faces/photos/`。 +3. 下载成功后保存到本地缓存目录: ```text -ApiClient -> GET /api/v1/faces/photos/{id} -> 本地 file:// 缓存 -> QML Image +/face_photos/.jpg ``` -这样可以在请求照片时携带 `X-Api-Token`,避免 QML `Image` 直接访问受鉴权接口导致照片加载失败。 +4. QML 使用本地 file URL 显示图片。 -### 4.9 关于页面 +## 15. 关于页面 -“关于”页展示: +关于页显示: -- 系统简介。 -- 功能范围。 -- 毕业设计说明。 -- 作者和仓库信息。 +1. 系统用途。 +2. 功能范围。 +3. 毕业设计说明。 +4. 作者和版权信息。 +5. 仓库地址。 +6. 演示账号信息。 -当前关于页是 QML 页面的一部分,不再是独立窗口。 +这些文本在 `QT/qml/Main.qml` 中。 -### 4.10 桌面端和移动端适配 +## 16. PC 和 Android 适配 -当前 QML 已对窄屏进行适配: +当前 QML 中有这些布局断点: -- `phoneLayout` 用于手机宽度布局。 -- 顶部导航在窄屏下使用紧凑按钮。 -- 筛选区、统计卡片和配置区会根据宽度调整列数。 -- 表格在窄屏下隐藏部分非关键列或调整宽度。 -- Android 端使用原生文字渲染并降低粗体权重,减少中文粗体重影。 - -## 5. 服务端 API 依赖 - -客户端基于服务端地址拼接 `/api` 前缀访问接口。刷新全部数据时优先使用统一同步接口: - -| 方法 | 路径 | 用途 | +| 属性 | 条件 | 用途 | | --- | --- | --- | -| `GET` | `/api/v1/sync/state?client=qt&last_version={version}` | 按版本同步 Qt 所需完整状态 | +| `compactLayout` | `width < 1460` | PC 紧凑布局 | +| `denseLayout` | `width < 1380` | 更密集显示 | +| `narrowLayout` | `width < 1280` | 表格列裁剪 | +| `slimLayout` | `width < 1040` | 隐藏侧边栏,使用移动导航 | +| `phoneLayout` | PC 小于 560,Android 小于 640 | 手机布局 | -服务端版本无变化时返回 `changed:false`,Qt 不再重复拉取全部数据。局部刷新和保存操作使用下列 v1 子接口。 +Android 特殊处理: -| 方法 | 路径 | 用途 | -| --- | --- | --- | -| `GET` | `/api/v1/attendance/records?limit={limit}&offset={offset}` | 获取考勤记录 | -| `GET` | `/api/v1/attendance/statistics` | 获取总数、今日数和服务端时间 | -| `POST` | `/api/v1/attendance/records/clear` | 清空全部记录 | -| `DELETE` | `/api/v1/attendance/records?timestamp={timestamp}&student_id={studentId}` | 删除单条记录,当前 API 层保留,界面未开放 | -| `GET` | `/api/v1/config/time-slots` | 获取签到时段 | -| `POST` | `/api/v1/config/time-slots` | 保存签到时段 | -| `GET` | `/api/v1/config/mappings` | 获取学生和班级映射 | -| `POST` | `/api/v1/config/mappings` | 保存学生和班级映射 | -| `GET` | `/api/v1/faces/mappings` | 获取人脸 ID 到学号映射 | -| `POST` | `/api/v1/faces/mappings` | 保存人脸 ID 到学号映射 | -| `GET` | `/api/v1/faces/photos` | 获取照片 ID 列表 | -| `GET` | `/api/v1/faces/student-ids` | 获取所有学生 ID | -| `GET` | `/api/v1/faces/photos/{id}` | 获取人脸照片 | -| `DELETE` | `/api/v1/faces/{studentId}` | 删除人脸信息 | -| `DELETE` | `/api/v1/faces/{studentId}?delete_records=true` | 删除人脸信息并删除相关考勤记录 | -| `GET` | `/api/v1/system/health` | 健康检查 | +1. 最小窗口宽度是 `360`。 +2. 最小窗口高度是 `560`。 +3. 字体使用 `sans-serif`。 +4. 粗体权重降低为 `Font.Medium`。 +5. 使用原生文字渲染,减少中文粗体重影。 -### 5.1 考勤记录格式 +配置页卡片现在使用内容自适应高度,避免按钮遮挡。 -`/api/v1/attendance/records` 解密后应返回 JSON 数组,单条记录示例: +## 17. 本地持久化 -```json -{ - "timestamp": "2026-04-24 08:10:23", - "student_id": "20240001", - "name": "张三", - "confidence": 0.9821, - "status": "success", - "class_id": "1" -} -``` +Qt 使用两类本地持久化。 -字段处理规则: +### 17.1 QSettings -- `timestamp` 需要符合 `yyyy-MM-dd HH:mm:ss`。 -- `student_id` 不能为空。 -- `class_id` 为空时客户端按 `"0"` 处理。 -- `status` 可以是字符串、布尔值或数字,客户端会统一转换为显示状态。 -- `timestamp` 以 `0000-` 开头的记录会被忽略。 - -### 5.2 统计数据格式 - -```json -{ - "total": 120, - "today": 8, - "server_time": "2026-04-24 10:30:00" -} -``` - -### 5.3 签到时段格式 - -```json -{ - "time_slots": [ - { - "name": "上午签到", - "start_time": "08:00", - "end_time": "09:30", - "enabled": true - } - ] -} -``` - -### 5.4 映射配置格式 - -```json -{ - "student_name_map": { - "20240001": "张三" - }, - "class_name_map": { - "1": "软件工程一班" - } -} -``` - -### 5.5 人脸映射格式 - -```json -{ - "mappings": { - "face_001": "20240001" - } -} -``` - -## 6. 加密响应 - -客户端对 `/api/v1/attendance/records` 和 `/api/v1/attendance/statistics` 请求添加请求头: +保存服务端地址: ```text -X-Encrypted-Response: true +server/url ``` -客户端期望服务端返回 Base64 编码的 AES-128-CBC 密文。收到响应后处理流程为: +### 17.2 AppDataLocation 文件 -1. Base64 解码。 -2. AES-128-CBC 解密。 -3. PKCS#7 去填充。 -4. 解析 JSON。 - -加密参数位于 `QT/src/CryptoUtils.cpp`: - -| 参数 | 值 | +| 文件 | 内容 | | --- | --- | -| 算法 | AES-128-CBC | -| Padding | PKCS#7 | -| Key | `2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c` | -| IV | `00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f` | +| `timeslot_config.json` | 时间段配置 | +| `mapping_config.json` | 学生姓名和班级名称映射 | +| `face_photos/*.jpg` | 下载后的人脸照片缓存 | -如果服务端没有按加密格式返回,记录和统计接口会解析失败。 +## 18. 构建方式 -## 7. 本地配置持久化 - -应用使用 `QSettings` 和 `QStandardPaths::AppDataLocation` 保存本地状态。 - -应用信息: - -```text -ApplicationName: 人脸识别考勤系统 -ApplicationVersion: 3.0.0 -OrganizationName: STM32 Face Recognition -``` - -本地保存内容: - -| 配置 | 保存方式 | -| --- | --- | -| 服务端地址 | `QSettings`,键名 `server/url` | -| 学生姓名映射和班级名称映射 | `mapping_config.json` | -| 签到时段配置 | `timeslot_config.json` | - -`mapping_config.json` 和 `timeslot_config.json` 位于 Qt 返回的 `AppDataLocation` 目录中,不在项目源码目录内。 - -## 8. 构建与运行 - -### 8.1 推荐构建方式 - -当前推荐使用 CMake 构建。旧版 `qmake` / `.pro` 文档不再适用于重构后的 Qt Quick 版本。 - -基础要求: - -- Qt 6.2 或更高版本 -- CMake 3.16 或更高版本 -- 支持 C++17 的编译器 - -Windows 桌面端推荐使用 Qt 6.x MinGW 64-bit Kit。 - -### 8.2 Qt Creator 构建 +### 18.1 Qt Creator 1. 打开 Qt Creator。 2. 打开 `QT/CMakeLists.txt`。 3. 选择 Desktop Qt 6.x 或 Android Kit。 4. 等待 CMake 配置完成。 -5. 构建目标 `apprewrite_qt`。 +5. 构建 `apprewrite_qt`。 6. 桌面端直接运行;Android 端部署到设备或模拟器。 -### 8.3 命令行构建 +### 18.2 命令行示例 -Windows 示例: +Windows: ```powershell -cmake -S QT -B QT/build -DCMAKE_PREFIX_PATH=D:/Qt/6.10.0/mingw_64 -cmake --build QT/build --target apprewrite_qt +cd D:\University\graduation\code\Reconstruct_Qt\QT +cmake -S . -B build\Desktop -DCMAKE_PREFIX_PATH=D:\Qt\6.10.0\mingw_64 +cmake --build build\Desktop ``` -Linux 示例: +实际路径要按本机 Qt 安装位置调整。 -```bash -cmake -S QT -B QT/build -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x/gcc_64 -cmake --build QT/build --target apprewrite_qt -``` - -安装: - -```bash -cmake --install QT/build -``` - -当前 `QT/CMakeLists.txt` 将运行产物输出到: - -```text -QT/dist -``` - -### 8.4 Windows 独立运行 - -直接双击 `apprewrite_qt.exe` 时,如果提示缺少 `Qt6Gui.dll`、`Qt6Core.dll` 等运行库,需要使用和构建 Kit 一致的 `windeployqt` 收集依赖。 - -示例: - -```powershell -D:\Qt\6.10.0\mingw_64\bin\windeployqt.exe QT\dist\apprewrite_qt.exe -``` - -如果路径包含中文导致部署工具异常,可以把 exe 复制到纯英文路径后部署。 - -### 8.5 Android 构建 - -Android 相关配置位于 `QT/CMakeLists.txt`: - -```cmake -QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android" -QT_ANDROID_APP_NAME "${REWRITE_QT_DISPLAY_NAME}" -QT_ANDROID_APP_ICON "@mipmap/ic_launcher" -``` - -补充配置位于 `QT/android/gradle.properties`: - -```properties -android.overridePathCheck=true -android.useAndroidX=true -``` - -说明: - -- `android.overridePathCheck=true` 用于绕过 Windows 下 Android Gradle 对中文路径的检查。 -- `android.useAndroidX=true` 用于启用 AndroidX。 -- Android 应用显示名为“人脸识别考勤系统”。 -- Android 启动图标位于 `QT/android/res/mipmap-*`。 - -## 9. 图标与资源 - -桌面图标资源位于: - -```text -QT/assets/favicon.ico -QT/assets/favicon-16x16.png -QT/assets/favicon-32x32.png -QT/assets/favicon-48x48.png -QT/assets/favicon-64x64.png -QT/assets/favicon-128x128.png -QT/assets/favicon-192x192.png -QT/assets/favicon-256x256.png -``` - -Android 启动图标位于: - -```text -QT/android/res/mipmap-mdpi/ic_launcher.png -QT/android/res/mipmap-hdpi/ic_launcher.png -QT/android/res/mipmap-xhdpi/ic_launcher.png -QT/android/res/mipmap-xxhdpi/ic_launcher.png -QT/android/res/mipmap-xxxhdpi/ic_launcher.png -``` - -Linux 桌面文件模板位于: - -```text -QT/cmake/rewrite_qt.desktop.in -``` - -Windows 图标资源模板位于: - -```text -QT/cmake/app_icon.rc.in -``` - -## 10. 当前已知限制 - -### 10.1 记录和统计最多基于当前加载的 1000 条记录 - -`AppController::refreshRecords()` 当前固定调用: - -```cpp -m_apiClient.fetchRecords(1000, 0); -``` - -因此记录列表、筛选结果和本地统计都只覆盖客户端当前加载的记录。如果服务端历史记录超过 `1000` 条,统计页不等于全量历史统计。 - -### 10.2 筛选和统计为本地计算 - -班级、学号、姓名、日期筛选以及统计分析都在客户端内存中完成,不会向服务端传递筛选条件。 - -优点是交互响应快;限制是数据范围受当前加载记录影响。 - -### 10.3 单条删除 API 保留但界面未开放 - -`ApiClient` 中保留了 `deleteRecord()` 和 `/api/v1/attendance/records` 调用,`AppController` 也连接了删除完成信号,但当前 QML 记录表没有提供单条删除或批量删除入口。 - -因此当前对外说明时,不应把“单条记录删除”或“批量删除记录”描述为已开放功能。 - -### 10.4 自动刷新不是定时拉取记录 - -当前代码实现的是服务端不可达后的自动重连检查,不是固定周期自动刷新记录。 - -用户需要手动点击“刷新”或“刷新全部数据”同步最新数据;服务恢复连接时也会自动刷新。 - -### 10.5 时间输入缺少强校验 - -签到时间段编辑框使用字符串输入,代码按 `HH:mm` 解析。如果输入非法时间,模型层不会弹出明确错误提示,可能产生无效时间值。后续可补充输入掩码或校验提示。 - -### 10.6 CSV 导出未做字段转义 - -导出逻辑直接用逗号拼接字段。如果姓名、班级名或状态中包含逗号、换行或引号,CSV 可能被表格软件解析成错误列。后续可补充标准 CSV 转义函数。 - -### 10.7 照片 URL 依赖服务端静态接口 - -人脸照片由 `ApiClient` 通过 `/api/v1/faces/photos/{id}` 携带鉴权头下载,并缓存为本地 `file://` 地址后交给 QML 显示。如果服务端返回格式或图片 ID 与学生 ID 规则变化,客户端的人脸列表合并逻辑需要同步调整。 - -## 11. 当前结论 - -重构后的 Qt 管理端已经从旧版 Widgets 多窗口结构迁移为 Qt 6 Quick/QML 单窗口客户端。当前核心链路已经完整: - -- 服务端连接与健康检查。 -- 加密记录和统计接口访问。 -- 考勤记录查看、筛选、导出、清空。 -- 本地统计分析和统计导出。 -- 服务端地址、时间段、学生映射、班级映射配置。 -- 人脸照片、人脸映射和删除管理。 -- Windows 桌面端与 Android 端构建配置。 - -当前主要不足集中在数据分页、全量统计、记录级删除入口、输入校验和 CSV 转义。后续如果要提升为更完整的管理端,应优先补充分页加载或服务端筛选统计接口,再补齐记录级管理操作。 +## 19. 常见问题 +| 现象 | 可能原因 | 处理 | +| --- | --- | --- | +| 测试连接失败 | 服务端没启动、地址错、Token 不一致 | 检查服务端健康接口和 `ATTENDANCE_API_TOKEN` | +| 页面按钮重叠 | 旧版固定高度导致内容被裁切 | 当前已改为自适应高度 | +| Android 文字发虚/重影 | 字体渲染方式问题 | 当前 Android 使用原生渲染和中等粗体 | +| 人脸照片不显示 | 下载失败、本地缓存路径无效、服务端无照片 | 刷新人脸库,看服务端 `faces/` | +| 统计数量少于服务端总量 | Qt 默认只拉取 1000 条记录 | 增加分页或服务端统计接口 | +| 时间段为空导致无法签到 | 当前默认就是空配置 | 在配置中心新增时间段并保存 | +| 保存映射后 K210 没变化 | K210 尚未同步或 WiFi 不通 | 等待同步或检查 K210 日志 | diff --git a/docs/05_项目审查问题清单.md b/docs/05_项目审查问题清单.md index b1037ec..128536a 100644 --- a/docs/05_项目审查问题清单.md +++ b/docs/05_项目审查问题清单.md @@ -1,344 +1,283 @@ -# 项目审查问题清单 +# 项目维护、验收与风险清单 -审查日期:2026-05-01 +## 1. 文档目的 -本文档用于记录当前项目中已发现的高风险问题,并作为后续逐项修复的跟踪清单。状态说明: +这份文档用于回答三个问题: -- `待处理`:尚未开始修复。 -- `处理中`:已经开始改动或验证。 -- `已修复`:代码已修复并通过基本验证。 -- `暂缓`:因演示环境、硬件依赖或其他原因暂不处理。 +1. 项目目前实现了什么。 +2. 验收或答辩时可以从哪些方面说明。 +3. 后续维护时有哪些风险和改进方向。 -## 1. 优先级概览 +它不是单纯的 bug 列表,而是把功能完整性、稳定性、安全性、易用性和可维护性都列出来。 -| 编号 | 优先级 | 状态 | 模块 | 问题 | -| --- | --- | --- | --- | --- | -| R-001 | P0 | 已修复 | 服务端 | 公网接口无鉴权且允许任意来源 | -| R-002 | P0 | 部分修复 | 全项目 | 明文密钥和凭据已进入项目文件 | -| R-003 | P1 | 已修复 | 服务端 | `face_id` 可造成路径穿越写读删文件 | -| R-004 | P1 | 已修复 | 服务端 | 大请求体会被静默截断 | -| R-005 | P1 | 已修复 | 服务端 / Qt | CSV/JSON 没有转义会破坏数据和响应 | -| R-006 | P1 | 已修复 | STM32 固件 | `sscanf` 未限制宽度,UART 输入可溢出栈缓冲 | -| R-007 | P2 | 已修复 | 服务端 | 多个 `std::stoi` 可被非法参数打崩服务线程 | -| R-008 | P2 | 已修复 | Qt 管理端 | 删除人脸 URL 未编码 | -| R-009 | P1 | 已修复 | STM32 固件 | NTP-only 模式下时间不会前进 | -| R-010 | P1 | 已修复 | STM32 固件 | 发送成功判断过早,可能静默丢签到 | -| R-011 | P1 | 已修复 | STM32 固件 | 今日缓存不区分时段,会误判重复签到 | -| R-012 | P1 | 已修复 | 服务端 | 签到请求手写 JSON 解析会被特殊字符破坏 | -| R-013 | P2 | 已修复 | K210 识别端 | 从 SD 卡加载特征时使用 `eval` | -| R-014 | P2 | 已修复 | 文档 | 总文档指向不存在的 Qt 工程文件 | -| R-015 | P2 | 已修复 | 服务端 / 数据 | 数据库缺少自动备份与恢复流程 | -| R-016 | P2 | 已修复 | 服务端 / K210 | 删除任务缺少 task_id 和状态跟踪 | -| R-017 | P2 | 已修复 | 服务端 / 多端同步 | 设备同步状态不可追踪 | -| R-018 | P2 | 已修复 | 部署 / 安全 | 公网 HTTP 明文传输缺少 HTTPS 或反向代理保护 | -| R-019 | P3 | 已修复 | 测试 | 关键接口缺少自动化回归脚本 | -| R-020 | P1 | 已修复 | K210 / 同步 | 删除 ACK 失败后本地版本仍前进,可能卡住删除任务 | -| R-021 | P2 | 已修复 | STM32 固件 / 多设备 | 多设备或缓存过期时重复签到仍可能漏判 | -| R-022 | P2 | 已修复 | STM32 固件 / ESP8266 | HTTP GET 可能接受半截响应 | +## 2. 当前实现状态总览 -## 2. 详细问题 +| 模块 | 当前状态 | 说明 | +| --- | --- | --- | +| STM32 主控 | 已实现 | 接收 K210、考勤判断、加密上传、断线重试、时间段同步 | +| K210 识别 | 已实现 | 人脸检测、特征比对、添加/删除、照片上传、映射同步 | +| 服务端 | 已实现 | SQLite 存储、HTTP API、Token、AES 解密、统一同步 | +| Qt 管理端 | 已实现 | 记录、统计、配置、人脸管理、PC/Android 响应式界面 | +| 默认时间段 | 已改为空 | 需要管理员在 Qt 中新增 | +| 数据导出 | 已实现 | Qt 可导出记录和统计 CSV | +| 自动化 API 回归 | 已有脚本 | `server/regression_api_tests.ps1` | -### R-001 公网接口无鉴权且允许任意来源 +## 3. 功能验收清单 -- 优先级:P0 -- 状态:已修复 -- 位置:`server/server_encrypted.cpp:1666` -- 影响:服务端暴露清空记录、删除人脸、上传映射、上传照片等写操作,但没有鉴权;响应固定允许任意跨域来源。只要端口可访问,外部请求就可能改写或删除考勤数据。 -- 建议处理: - 1. 给所有写接口增加 API Token 或签名校验。 - 2. 按部署环境限制 CORS 来源,不再使用通配符。 - 3. 公网部署时使用 HTTPS 和反向代理鉴权。 - 4. 将管理类接口与设备上传接口区分权限。 -- 本次处理:主服务端 `server/server_encrypted.cpp` 已增加 `X-Api-Token` 校验,健康检查和 OPTIONS 预检除外;CORS 来源改为 `ATTENDANCE_CORS_ORIGIN` 配置,默认仅本地来源。Qt、STM32、K210 请求均已补 `X-Api-Token`。 +### 3.1 服务端验收 -### R-002 明文密钥和凭据已进入项目文件 +| 检查项 | 通过标准 | +| --- | --- | +| 服务可启动 | 控制台显示监听 `8082` | +| 健康检查 | `/api/v1/system/health` 返回 `healthy` | +| SQLite 初始化 | 生成或打开 `attendance.db` | +| 时间接口 | `/api/v1/system/time` 返回 `epoch` 和 `server_time` | +| Token 鉴权 | 不带 Token 访问受保护接口返回 401 | +| 时间段默认 | `/api/v1/config/time-slots` 返回 `{"time_slots":[]}` | +| 记录接口 | `/api/v1/attendance/records` 返回 JSON 数组 | +| 统计接口 | `/api/v1/attendance/statistics` 返回 `total`、`today` | +| 照片接口 | 可列出、上传、下载 JPEG | +| 同步接口 | `/api/v1/sync/state` 按客户端返回不同数据 | -- 优先级:P0 -- 状态:部分修复 -- 位置:`k210/main.py:34`、`User/main.c:23`、`QT/qml/Main.qml:2725`、`server/attendance.log` -- 影响:项目文件中包含 WiFi 密码、公网服务地址、固定 AES Key/IV、查看账号密码、日志中的疑似 API Key 等敏感信息。泄露后即使代码修复,旧凭据仍可能被继续使用。 -- 建议处理: - 1. 立即轮换已暴露的 WiFi 密码、API Key、演示账号密码和 AES Key/IV。 - 2. 将服务地址、WiFi、Token 等改为外部配置文件或烧录参数。 - 3. 日志输出中屏蔽敏感请求头。 - 4. 清理已有日志文件中的敏感内容。 -- 本次处理:主链路代码和文档中的 WiFi 密码、公网地址改为占位配置;Qt 关于页移除固定演示账号密码;日志中的疑似 API Key 已脱敏;Qt 默认服务地址改为本机地址。仍需在真实环境中轮换已暴露的 WiFi、API Key、演示账号和 AES Key/IV。 +### 3.2 Qt 管理端验收 -### R-003 `face_id` 可造成路径穿越写读删文件 +| 检查项 | 通过标准 | +| --- | --- | +| 启动界面 | Windows 和 Android 都能打开主界面 | +| 服务地址 | 可保存地址并测试连接 | +| 配置页布局 | PC 和 Android 不出现按钮遮挡 | +| 时间段 | 默认为空,可新增、保存、同步、清空 | +| 映射 | 可新增学生姓名和班级名称映射 | +| 记录页 | 能加载、筛选和导出记录 | +| 统计页 | 能按日期范围统计并导出 | +| 人脸管理 | 能刷新照片、绑定学号、保存映射、删除人脸 | +| 自动重连 | 服务端恢复后能重新同步 | -- 优先级:P1 -- 状态:已修复 -- 位置:`server/server_encrypted.cpp:837` -- 影响:`face_id` 直接来自 URL 并拼接成 `faces/.jpg`,用于写入、读取和删除。包含路径分隔符或 `..` 时,可能越过 `faces` 目录操作其他文件。 -- 建议处理: - 1. 增加 `face_id` 白名单校验,只允许数字、字母、下划线、短横线等明确字符。 - 2. 对最终路径做规范化检查,确认仍位于 `faces` 目录下。 - 3. 所有照片读写删入口共用同一个安全路径构造函数。 -- 本次处理:主服务端已增加 `isSafeId` 与 `buildFacePhotoPath`,照片读写删和重命名路径统一走安全 ID 校验。 +### 3.3 STM32 验收 -### R-004 大请求体会被静默截断 +| 检查项 | 通过标准 | +| --- | --- | +| 硬件初始化 | OLED、蜂鸣器、LED、串口正常 | +| ESP8266 | 能连接 WiFi 和服务端 | +| 时间同步 | 能从服务端获取时间或读取 DS1302 | +| 时间段同步 | 能拉取服务端配置,空配置会清空本地时间段 | +| K210 接收 | 能解析 `ID:...,NAME:...,CONF:...` | +| 签到判断 | 置信度、时间段、重复签到逻辑生效 | +| 加密上传 | 服务端能收到并解密保存记录 | +| 断线重试 | 网络断开时记录保留,恢复后同步 | -- 优先级:P1 -- 状态:已修复 -- 位置:`server/server_encrypted.cpp:1228` -- 影响:读取 POST body 时,如果剩余数据超过约 40KB,循环不会继续读取,导致照片或配置内容被截断。K210 JPEG 上传很容易触发该问题。 -- 建议处理: - 1. 按 `Content-Length` 循环读取完整 body。 - 2. 设置明确最大请求大小,例如照片接口单独允许更大上限。 - 3. 超限时返回 HTTP 413。 - 4. 对读取不足的请求返回 400,不保存半截文件。 -- 本次处理:主服务端按 `Content-Length` 读取完整请求体,普通 JSON 请求限制 64KB,照片请求限制 512KB,超限返回 413,读取不足返回 400。 +### 3.4 K210 验收 -### R-005 CSV/JSON 没有转义会破坏数据和响应 +| 检查项 | 通过标准 | +| --- | --- | +| 模型加载 | 三个 `.smodel` 正常加载 | +| 摄像头 | LCD 能显示画面,方向正确 | +| 人脸识别 | 已录入人脸能识别,陌生人显示陌生 | +| 添加人脸 | 短按进入添加,确认后写入 SD 卡 | +| 删除人脸 | 长按进入删除,可删除单个或全部 | +| 照片上传 | 新增人脸照片能上传到服务端 | +| 映射同步 | Qt 保存映射后 K210 本地名称更新 | +| 删除同步 | Qt 删除后 K210 能删除本地特征并 ACK | -- 优先级:P1 -- 状态:已修复 -- 位置:`server/server_encrypted.cpp:459`、`QT/src/AppController.cpp:284`、`QT/src/AppController.cpp:317` -- 影响:签到记录直接用逗号拼 CSV,读取时也直接按逗号拆,再手写 JSON 字符串。姓名、状态、班级等字段包含逗号、引号、换行或反斜杠时,会导致 CSV 列错位、JSON 非法或 Qt 解析失败。 -- 建议处理: - 1. 服务端增加 CSV 字段转义和解析函数。 - 2. 服务端 JSON 响应统一经过 JSON 转义函数。 - 3. Qt 导出 CSV 时对每个字段做标准 CSV 转义。 - 4. 使用 SQLite 替代 CSV/多 JSON 文件,减少数据状态不一致。 -- 本次处理:主服务端增加 CSV 解析/转义和 JSON 转义;Qt 导出 CSV 已对字段做标准转义;后端主存储已迁移到 `attendance.db`,旧 CSV/JSON 仅作为首次导入来源。 +## 4. 数据一致性清单 -### R-006 `sscanf` 未限制宽度,UART 输入可溢出栈缓冲 +### 4.1 Token 一致 -- 优先级:P1 -- 状态:已修复 -- 位置:`Hardware/K210_USART.c:80` -- 影响:`%[^,]` 没有宽度限制,但目标缓冲只有 `id[20]` 和 `name[32]`。串口收到异常长字段时可能覆盖栈内存,造成固件异常。 -- 建议处理: - 1. 将格式改为 `%19[^,]`、`%31[^,]`。 - 2. 对接收缓冲做最大帧长、换行结束符和字段合法性校验。 - 3. 异常帧直接丢弃并清空缓冲。 -- 本次处理:`Hardware/K210_USART.c` 和示例文件中的 `sscanf` 已增加字段宽度限制。 +这些地方必须一致: -### R-007 多个 `std::stoi` 可被非法参数打崩服务线程 +1. 服务端 `ATTENDANCE_API_TOKEN` 或默认 Token。 +2. Qt 环境变量或默认 Token。 +3. `User/main.c` 的 `HTTP_API_TOKEN`。 +4. `k210/main.py` 的 `API_TOKEN`。 -- 优先级:P2 -- 状态:已修复 -- 位置:`server/server_encrypted.cpp:1310` -- 影响:`limit`、`offset`、时间段参数直接调用 `std::stoi`,没有异常捕获和范围校验。非法请求参数可能让请求处理线程异常终止。 -- 建议处理: - 1. 增加安全整数解析函数。 - 2. 参数非法时返回 400。 - 3. 限制 `limit`、`offset` 和时间范围。 - 4. 所有 URL 参数统一走校验函数。 -- 本次处理:主服务端已增加安全整数和时间解析,`limit`、`offset`、时间段和记录时间解析不再直接裸用 `std::stoi`。 +### 4.2 服务端地址一致 -### R-008 删除人脸 URL 未编码 +这些地方要指向同一个服务端: -- 优先级:P2 -- 状态:已修复 -- 位置:`QT/src/ApiClient.cpp:453` -- 影响:Qt 端删除人脸时直接把 `studentId` 拼进路径。如果学号包含空格、`#`、`?`、`/` 或中文,URL 会被截断或路由错乱。 -- 建议处理: - 1. 使用 `QUrl::toPercentEncoding` 编码路径段。 - 2. 服务端接收后进行 URL 解码和 ID 校验。 - 3. 为删除人脸接口补充异常 ID 的测试用例。 -- 本次处理:Qt 删除人脸路径段已使用 `QUrl::toPercentEncoding` 编码。 +1. Qt 配置中心的服务端地址。 +2. `User/main.c` 的 `HTTP_SERVER_IP` 和 `HTTP_SERVER_PORT`。 +3. `k210/main.py` 的 `SERVER_IP` 和 `SERVER_PORT`。 -### R-009 NTP-only 模式下时间不会前进 +### 4.3 AES 参数一致 -- 优先级:P1 -- 状态:已修复 -- 位置:`User/main.c:467-472` -- 影响:DS1302 不可用时,系统每秒只是把 `g_current_time.hour/minute` 再写入考勤模块,没有按系统 tick 推进秒、分钟和日期。同步一次 NTP 后,考勤时间会冻结在同步瞬间,后续时段判断可能长期错误。 -- 建议处理: - 1. 在 NTP-only 模式下基于 `Get_Time_ms()` 维护软件时钟。 - 2. 每秒递增 `g_current_time`,正确处理分钟、小时、日期进位。 - 3. NTP 同步成功时重置软件时钟基准。 - 4. 增加跨分钟、跨小时、跨天的基本验证。 -- 本次处理:`User/main.c` 增加软件时钟推进逻辑,DS1302 不可用时按 `Get_Time_ms()` 每秒递增秒、分钟、小时和日期;NTP 同步成功后重置推进基准。 +这些文件必须一致: -### R-010 发送成功判断过早,可能静默丢签到 +1. `Hardware/Crypto.c` +2. `server/server_encrypted.cpp` +3. `QT/src/CryptoUtils.cpp` -- 优先级:P1 -- 状态:已修复 -- 位置:`Hardware/esp8266.c:230-236` -- 影响:STM32 侧 POST 只等待 ESP8266 的 `>` 提示并写出 HTTP 字符串,随后立即返回成功;没有等待 `SEND OK`,也没有检查服务端 HTTP 200 或 JSON `success`。网络中断、服务端 401/500、请求体未落库时,调用方仍可能把记录标记为已同步,造成静默丢签到。 -- 建议处理: - 1. 发送后等待 `SEND OK`,失败时返回 `ESP8266_FAIL`。 - 2. 继续读取 HTTP 响应,确认状态码为 200 且响应体包含成功标志。 - 3. 超时、401、500、响应解析失败都应保留记录为待同步或失败待重试。 - 4. 记录服务端返回的错误信息,便于联调排查 Token、加密和数据库问题。 -- 本次处理:`Hardware/esp8266.c` 的加密 POST 发送后会等待 `SEND OK`、HTTP 200 和响应体 `success:true`;超时、发送失败、401/400/413/500 或 `success:false` 都返回失败,使记录保留为待重试。 +参数: -### R-011 今日缓存不区分时段,会误判重复签到 +```text +Key: 2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c +IV : 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f +``` -- 优先级:P1 -- 状态:已修复 -- 位置:`Hardware/Attendance.c:75-108` -- 影响:当前缓存只保存 `student_id`,不保存考勤时段。任一时段签到成功后都会进入缓存,后续中午、下午、晚上时段再签到可能被 `Attendance_IsDuplicateInCache` 判为重复。开机不在时段时还会按 `00:00-23:59` 拉取全天记录,误判范围更大。 -- 建议处理: - 1. 缓存项增加时段编号或 `slot_start/slot_end` 字段。 - 2. 本地新增签到写缓存时同时写入当前时段。 - 3. 服务器下载今日记录时只填充当前有效时段的缓存;不在时段内不要拉取全天缓存用于查重。 - 4. 切换时段时清理或重建缓存,避免跨时段污染。 -- 本次处理:`Hardware/Attendance.c` 的缓存项增加时段绑定;新增签到和服务器下载缓存都写入当前时段;不在时段内不再拉取全天缓存;时段切换时自动清空缓存。 +### 4.4 时间段一致 -### R-012 签到请求手写 JSON 解析会被特殊字符破坏 +Qt 保存时间段后: -- 优先级:P1 -- 状态:已修复 -- 位置:`server/server_encrypted.cpp:707-742` -- 影响:`parseJSON` 直接按逗号拆字段、按第一个冒号拆键值,无法正确处理字符串里的逗号、冒号、反斜杠和转义引号。签到姓名、状态或后续字段一旦包含这些字符,会解析错列甚至写入错误数据。 -- 建议处理: - 1. 引入 `nlohmann/json` 等成熟 JSON 库,优先替换签到请求体解析。 - 2. 对必填字段做类型、长度和取值校验。 - 3. JSON 解析失败时返回 400,避免写入半解析数据。 - 4. 增加包含逗号、冒号、引号和反斜杠的解析测试样例。 -- 本次处理:`server/server_encrypted.cpp` 的签到 JSON 解析改为逐字符解析字符串字面量,支持逗号、冒号、反斜杠和转义引号;签到字段增加 ID、长度、置信度范围和状态取值校验,非法请求返回 400。 +1. 服务端 `time_slots` 更新。 +2. 服务端同步版本提升。 +3. STM32 下一次同步拉取到新时间段。 +4. STM32 使用新的时间段判断签到。 -### R-013 K210 从 SD 卡加载特征时使用 `eval` +### 4.5 人脸映射一致 -- 优先级:P2 -- 状态:已修复 -- 位置:`k210/main.py:1315-1321` -- 影响:`recordftr.txt` 内容来自可写 SD 卡,`eval(parts[1])` 会执行任意 Python 表达式。文件损坏或被替换时,可能执行意外代码、卡死或耗尽内存。 -- 建议处理: - 1. 如果 MaixPy 支持,改用 `ast.literal_eval`。 - 2. 如果不支持,改为明确的浮点数组解析器,只接受数字、逗号、方括号和空白。 - 3. 校验特征向量长度和数值范围,异常记录跳过并打印错误。 - 4. 保存特征时考虑使用更容易校验的 JSON 或二进制格式。 -- 本次处理:`k210/main.py` 已用 `parse_feature_vector` 替换 `eval`,只接受浮点数组允许字符,并校验向量长度,异常记录会被跳过并打印解析错误。 +Qt 保存人脸映射后: -### R-014 总文档指向不存在的 Qt 工程文件 +1. 服务端保存 `face_mappings`。 +2. 服务端触发 K210 同步请求。 +3. K210 同步后更新 `face_mapping.txt`。 +4. K210 更新 `recordftr.txt`。 +5. K210 发给 STM32 的 ID 变成真实学号。 -- 优先级:P2 -- 状态:已修复 -- 位置:`docs/00_项目总文档.md:15` -- 影响:总文档写的是用 `QT/AttendanceViewer.pro` 构建,但仓库里没有该文件;实际 Qt 项目使用 `QT/CMakeLists.txt`。部署、答辩复现或新成员接手时会被误导。 -- 建议处理: - 1. 将总文档中的 Qt 构建入口改为 `QT/CMakeLists.txt`。 - 2. 与 `docs/04_Qt管理端文档.md` 保持一致,说明当前推荐 CMake 构建。 - 3. 全仓库搜索旧 `.pro`、`qmake` 和旧接口路径,清理过时说明。 -- 本次处理:总文档中的 Qt 构建入口已改为 `QT/CMakeLists.txt`;删除 `docs/07_后续优化路线.md` 前同步清理了相关引用。 +## 5. 当前已知限制 -### R-015 数据库缺少自动备份与恢复流程 +### 5.1 Qt 记录统计基于当前加载记录 -- 优先级:P2 -- 状态:已修复 -- 位置:`server/server_encrypted.cpp`、`docs/03_服务端文档.md` -- 影响:当前服务端使用 `attendance.db` 作为主存储,但没有自动备份、恢复步骤和误删后的应急流程。调试、答辩演示或清空记录接口误操作时,可能导致考勤记录、映射和同步状态难以恢复。 -- 建议处理: - 1. 增加服务端启动前或每日首次启动的 SQLite 备份逻辑。 - 2. 在服务端文档中补充停止服务、备份、恢复和校验步骤。 - 3. Qt 端后续可增加导出数据库备份入口。 - 4. 备份文件应带日期时间后缀,避免覆盖最近可用备份。 -- 本次处理:服务端启动时会把已有 `attendance.db` 复制到 `backups/attendance_YYYY-MM-DD_HH-MM-SS.db`,备份失败则停止启动;`docs/03_服务端文档.md` 已补恢复和校验流程。 +Qt 默认请求: -### R-016 删除任务缺少 task_id 和状态跟踪 +```text +limit=1000&offset=0 +``` -- 优先级:P2 -- 状态:已修复 -- 位置:`server/server_encrypted.cpp`、`k210/main.py` -- 影响:当前待删除任务主要按 `face_id` 列表流转,K210 删除成功后按 ID ACK。重复删除、删除后重新添加同 ID、多设备并发同步时,可能难以区分旧任务和新任务,也不方便追踪任务创建、下发、确认和失败状态。 -- 建议处理: - 1. 将删除任务升级为带 `task_id`、`face_id`、`student_id`、`created_at`、`status`、`ack_at` 的记录。 - 2. K210 ACK 时回传 `task_id`,服务端按任务确认。 - 3. 保留兼容旧 `face_id` ACK 的过渡逻辑,避免一次性破坏现有联调链路。 - 4. 在 Qt 或服务端状态接口中暴露待删除任务数量和最近任务状态。 -- 本次处理:服务端 `pending_delete` 升级为任务表,包含 `task_id`、`face_id`、`student_id`、`created_at`、`status`、`ack_at`;同步响应同时返回兼容旧数组和 `delete_tasks`;K210 ACK 会回传 `task_ids`,服务端仍兼容旧 `face_id` 确认。 +如果服务端已有超过 1000 条记录,Qt 本地筛选和统计只覆盖当前加载的这 1000 条。服务端 `/statistics` 的总数和今日数仍来自数据库。 -### R-017 设备同步状态不可追踪 +改进方向: -- 优先级:P2 -- 状态:已修复 -- 位置:`server/server_encrypted.cpp` -- 影响:各端只在本地保存自己的 `last_version`,服务端不知道 STM32、K210、Qt 最后看到或确认到了哪个同步版本。设备长期离线、同步卡住、版本落后时,只能靠日志和现场排查,联调成本较高。 -- 建议处理: - 1. 增加 `device_sync_state` 表,记录 `client`、`last_seen_version`、`last_ack_version`、`last_seen_at`。 - 2. 在 `/api/v1/sync/state` 和 `/api/v1/sync/ack` 中更新设备状态。 - 3. 在 `/api/v1/system/storage` 中返回设备同步摘要。 - 4. 后续 Qt 可展示同步状态,但不要求作为首阶段必须项。 -- 本次处理:服务端新增 `device_sync_state` 表;`/api/v1/sync/state` 记录设备最近看到的服务端版本,`/api/v1/sync/ack` 记录 ACK 版本;`/api/v1/system/storage` 返回 `device_sync_state` 摘要。 +1. 增加分页。 +2. 增加服务端按条件查询。 +3. 增加服务端按日期统计接口。 -### R-018 公网 HTTP 明文传输缺少 HTTPS 或反向代理保护 +### 5.2 时间输入缺少强校验 -- 优先级:P2 -- 状态:已修复 -- 位置:`server/server_encrypted.cpp`、部署配置 -- 影响:服务端当前直接提供 HTTP 8082。若部署在公网,`X-Api-Token`、配置接口、照片上传下载和管理操作都会以明文传输,Token 可能被窃听或重放。AES 签到正文只覆盖 STM32 上传内容,不能保护管理 API 和设备同步 API。 -- 建议处理: - 1. 公网部署时使用 Nginx/Caddy 等反向代理终止 HTTPS。 - 2. 服务端尽量只监听本机或内网地址,由反向代理对外暴露。 - 3. 配置 HTTPS 后轮换 API Token 和相关密钥。 - 4. 文档中明确本机演示和公网部署的不同安全要求。 -- 本次处理:服务端增加 `ATTENDANCE_BIND_HOST` 监听地址配置;文档新增公网部署安全要求,建议服务端绑定 `127.0.0.1` 并通过 Nginx/Caddy 终止 HTTPS 后再对外暴露。 +Qt 时间段输入框允许手动输入文本。当前主要依赖 `QTime::fromString("HH:mm")` 解析。 -### R-019 关键接口缺少自动化回归脚本 +改进方向: -- 优先级:P3 -- 状态:已修复 -- 位置:`docs/06_后端接口Windows测试命令.md`、待新增测试脚本 -- 影响:当前主要依赖手动 curl 文档验证接口。服务端、Qt、STM32、K210 协议经常一起变动,缺少脚本化回归会让鉴权、同步状态、时间段配置、照片接口和记录接口的破坏更晚被发现。 -- 建议处理: - 1. 保留现有手动 curl 文档,同时增加 PowerShell 自动化测试脚本。 - 2. 覆盖 health、storage、records、statistics、time-slots、sync/state、faces/photos 等低风险接口。 - 3. 对需要真实硬件或真实 AES 正文的接口保留手动验证说明。 - 4. 在每次服务端改动后运行脚本,记录测试结果。 -- 本次处理:新增 `server/regression_api_tests.ps1`,覆盖 health、storage、time、sync、records、statistics、time-slots、duplicates、today、mappings、faces 等低风险接口;手动命令文档已加入脚本使用方式。 +1. 使用时间选择控件。 +2. 保存前校验格式。 +3. 检查开始时间必须早于结束时间。 +4. 检查时间段重叠。 -### R-020 删除 ACK 失败后本地版本仍前进,可能卡住删除任务 +### 5.3 K210 JSON 解析仍偏轻量 -- 优先级:P1 -- 状态:已修复 -- 位置:`k210/main.py:1227-1239` -- 影响:K210 删除本地人脸后会向服务端发送同步 ACK,但当前没有判断 ACK 响应是否成功,就保存了 `server_version`。如果 ACK 请求丢包、超时或服务端未确认,服务端仍保留待删除任务;K210 下一次用已推进的 `last_version` 请求同步时可能得到 `changed:false`,导致该删除任务长期留在服务端,后续同步状态不一致。 -- 建议处理: - 1. 检查 `ack_resp` 是否包含成功标志,例如 `"success":true`。 - 2. 只有删除成功且 ACK 成功时,才保存新的 `sync_last_version`。 - 3. ACK 失败时保留旧版本,并打印日志,等待下一轮同步重试。 - 4. 可在服务端状态接口或 Qt 端观察 pending delete 数量,验证任务不会卡住。 -- 备注:这是联调稳定性问题,不属于必须复杂化同步协议的工业级需求;毕业设计中用简单 ACK 成功判断即可。 -- 本次处理:`k210/main.py` 在删除任务 ACK 后检查响应中是否包含 `"success":true`;ACK 失败时将本轮同步标记为失败,不保存新的本地同步版本,等待下一轮继续重试。 +K210 MicroPython 环境中很多 JSON 解析是手写字符串处理。简单数据够用,但复杂转义字符支持有限。 -### R-021 多设备或缓存过期时重复签到仍可能漏判 +改进方向: -- 优先级:P2 -- 状态:已修复 -- 位置:`Hardware/Attendance.c:520-529` -- 影响:当前签到处理主要检查 STM32 本地记录和开机/重连时下载的今日缓存。`Attendance_IsDuplicateOnServer` 已存在,但没有接入主签到路径。如果现场只有一台设备演示,影响较小;如果多台设备同时使用,或设备启动后缓存长时间未刷新,服务端已有的同一时段签到记录可能不会被当前设备发现,导致重复签到。 -- 建议处理: - 1. 在 `g_server_connected` 为真时,签到前调用一次 `Attendance_IsDuplicateOnServer`。 - 2. 查询失败时仍允许本地签到并进入待同步队列,避免弱网影响正常演示。 - 3. 时段切换、服务器重连成功后刷新今日记录缓存。 - 4. 如果只保留单设备演示,也应在论文或文档中说明本地缓存查重的适用范围。 -- 备注:毕业设计阶段不需要做复杂分布式一致性,优先保证演示环境下行为可解释。 -- 本次处理:`Hardware/Attendance.c` 在本地记录和缓存查重之后,服务器在线时调用 `Attendance_IsDuplicateOnServer` 做一次服务端查重;查询失败仍按未重复处理,避免弱网阻断演示。`User/main.c` 在 WiFi/HTTP 服务端重连成功后刷新时间段配置和今日记录缓存。 +1. 尽量让服务端返回简单结构。 +2. 避免映射 key/value 中出现引号、逗号等复杂字符。 +3. 如果固件环境支持,使用 JSON 库。 -### R-022 HTTP GET 可能接受半截响应 +### 5.4 STM32 本地缓存容量有限 -- 优先级:P2 -- 状态:已修复 -- 位置:`Hardware/esp8266.c:486-498` -- 影响:`ESP8266_HttpGet` 在发现响应体后,如果超过约 3 秒且 `body_len > 0`,即使存在 `Content-Length` 且尚未收完整,也会复制响应并返回成功。弱网、ESP8266 分包或服务端响应稍慢时,STM32 可能拿到半截 JSON,进而导致时间段配置、同步状态或今日记录解析失败。 -- 建议处理: - 1. 如果响应头包含 `Content-Length`,必须等到 `body_len >= content_length` 才返回成功。 - 2. 只有没有 `Content-Length` 时,才使用“超时且已有响应体”的兜底逻辑。 - 3. 超时未收完整时返回 `ESP8266_FAIL`,让上层保留当前配置并下次重试。 - 4. 用较大的同步状态响应或人工延迟响应测试 ESP8266 分包场景。 -- 备注:该项主要提升弱网稳定性,不影响当前本地小数据量演示的主体功能。 -- 本次处理:`Hardware/esp8266.c` 的 GET 响应处理改为:只在没有 `Content-Length` 时使用“超时且已有响应体”的兜底逻辑;如果服务端返回了 `Content-Length`,必须等到响应体长度达到声明长度才返回成功。 +STM32 本地记录最多 100 条,今日学生缓存最多 50 个 ID。 -## 3. 建议修复顺序 +改进方向: -1. 已完成 R-020:K210 删除 ACK 失败时不再推进本地同步版本,避免服务端待删除任务卡住。 -2. 已完成 R-022:ESP8266 GET 在有 `Content-Length` 时必须收完整响应,减少弱网下配置同步解析失败。 -3. 已完成 R-021:STM32 在线签到前补服务端查重,重连后刷新今日记录缓存。 -4. R-002 仍需真实环境人工轮换已暴露凭据;代码层面已尽量外部配置化和脱敏。 -5. 已修复项保持回归关注:R-001、R-003、R-004、R-005、R-006、R-007、R-008、R-009 至 R-022。 +1. 根据实际班级人数扩大缓存。 +2. 增加外部 Flash 或 SD 卡存储。 +3. 对已同步记录做更明确的淘汰策略。 -## 4. 验证记录 +### 5.5 服务端是单文件原生 HTTP 实现 + +服务端用 socket 手写 HTTP,演示和毕业设计足够直观,但在生产环境上不如成熟 Web 框架。 + +改进方向: + +1. 引入成熟 HTTP 库。 +2. 增加结构化日志。 +3. 增加配置文件。 +4. 增加单元测试。 + +## 6. 安全风险和建议 + +| 风险 | 当前状态 | 建议 | +| --- | --- | --- | +| 默认 Token 写在代码中 | 存在 | 部署时用环境变量,并替换 STM32/K210 常量 | +| HTTP 明文传输 | 存在 | 公网部署加 HTTPS 反向代理 | +| AES Key/IV 固定 | 存在 | 演示可接受,生产应使用更安全的密钥管理 | +| 管理接口权限粗粒度 | 存在 | 增加用户登录、角色权限 | +| 清空记录接口风险高 | 存在 | 增加二次确认、审计日志 | +| 删除人脸会影响多端 | 已有同步任务 | 保留日志,避免误删 | + +## 7. 答辩或展示可以讲的重点 + +### 7.1 多端协同 + +项目不是单独做一个 UI 或一个识别脚本,而是把嵌入式主控、AI 识别、服务端、桌面/移动管理端串起来。 + +### 7.2 安全传输 + +STM32 上传签到数据不是明文,而是 AES-128-CBC 加密后 Base64 传输。服务端负责解密和校验。 + +### 7.3 同步版本机制 + +服务端维护统一版本号,STM32、K210、Qt 都可以按版本拉取自己需要的数据,减少无意义请求。 + +### 7.4 断线容错 + +STM32 在网络异常时仍能运行,并保留待同步记录;K210 在照片上传失败时保存本地照片并重新排队。 + +### 7.5 跨平台管理端 + +Qt 管理端支持 PC 和 Android,配置页已做响应式布局,避免窄屏按钮遮挡。 + +### 7.6 数据可视化和导出 + +Qt 提供记录筛选、统计分析和 CSV 导出,方便后续汇总考勤数据。 + +## 8. 回归测试建议 + +### 8.1 每次改服务端后 + +1. 启动服务端。 +2. 运行 `server/regression_api_tests.ps1`。 +3. 手动测试上传时间段为空数组。 +4. 手动测试保存人脸映射后同步版本是否变化。 +5. 手动测试删除人脸后 `pending_delete` 是否出现任务。 + +### 8.2 每次改 Qt 后 + +1. 检查 PC 宽屏。 +2. 检查 PC 窄屏。 +3. 检查 Android 手机宽度。 +4. 检查配置中心展开/收起。 +5. 检查时间段为空时页面是否正常。 +6. 检查保存映射和刷新照片。 + +### 8.3 每次改 STM32 后 + +1. 看调试串口启动日志。 +2. 确认 ESP8266 能连 WiFi。 +3. 确认能下载时间段。 +4. 确认空时间段时不会签到。 +5. 配置时间段后再确认能签到。 +6. 断开服务端再恢复,检查待同步记录上传。 + +### 8.4 每次改 K210 后 + +1. 确认模型加载。 +2. 确认添加人脸写入 SD 卡。 +3. 确认照片进入上传队列。 +4. 确认服务端能看到照片。 +5. 确认 Qt 保存映射后 K210 能同步。 +6. 确认 Qt 删除后 K210 能删除并 ACK。 + +## 9. 建议后续优化路线 + +优先级从高到低: + +1. 增加 Qt 时间输入校验和重叠检测。 +2. 给服务端增加配置文件,减少硬编码。 +3. 给服务端增加更完整的自动化测试。 +4. 给 Qt 增加记录分页。 +5. 给服务端增加按条件查询和统计接口。 +6. 把 Token 和 AES 密钥改成部署时配置。 +7. 增加 HTTPS 部署说明。 +8. 给 STM32 增加更大的离线缓存。 +9. 给 K210 使用更稳健的 JSON 解析。 +10. 增加服务端日志文件轮转。 -- `k210/main.py`、`k210/view_attendance.py` 已通过普通 Python 语法检查;MaixPy 运行时仍需上板验证。 -- `server/server_encrypted.cpp` 因当前本机缺少 OpenSSL 头文件,未完成语法编译验证。 -- 现有 Keil 构建日志显示固件旧构建为 `0 Error(s), 0 Warning(s)`,但尚未在本次审查中重新编译固件。 -- 2026-05-01:已完成主链路安全修复;旧版备用服务端与测试脚本已清理,避免误部署非鉴权接口。 -- 2026-05-01:追加记录 R-009 至 R-014,作为后续优化和修复跟踪项。 -- 2026-05-01:分析并合并 `docs/07_后续优化路线.md` 中仍需跟踪的项目,追加 R-015 至 R-019;同步删除该路线文档。 -- 2026-05-01:完成 R-009 至 R-013、R-015 至 R-019 的代码和文档修复;新增服务端回归脚本 `server/regression_api_tests.ps1`。仍需上板验证 STM32/K210 运行时行为,并在真实部署中轮换 R-002 涉及的凭据。 -- 2026-05-01:追加记录 R-020 至 R-022,作为后续优化跟踪项。 -- 2026-05-01:完成 R-020 至 R-022 的代码修复;`k210/main.py` 已通过普通 Python 语法检查,STM32 相关改动仍需 Keil 编译和上板验证。 diff --git a/docs/06_后端接口Windows测试命令.md b/docs/06_后端接口Windows测试命令.md index 7a0c652..9b9f051 100644 --- a/docs/06_后端接口Windows测试命令.md +++ b/docs/06_后端接口Windows测试命令.md @@ -1,340 +1,546 @@ -# 后端接口 Windows 手动测试命令 +# 后端接口 Windows 测试命令 -当前后端 `server_encrypted.cpp` 只监听 HTTP 端口: +## 1. 使用前准备 -```text -8082 -``` +本文档用于在 Windows PowerShell 中手动测试服务端接口。 -以下命令适合在 Windows PowerShell 或 CMD 中复制执行。Windows 下建议使用 `curl.exe`,避免 PowerShell 把 `curl` 解析成别名。 - -如果后端在本机运行,使用: - -```text -http://127.0.0.1:8082 -``` - -如果测试公网服务器,替换成: - -```text -https://your-domain.example -``` - -下面示例默认使用本机地址。 - -## 0. 自动化回归脚本 - -服务端启动后,优先运行脚本化检查: +默认服务端地址: ```powershell -cd server -powershell -ExecutionPolicy Bypass -File .\regression_api_tests.ps1 -BaseUrl "http://127.0.0.1:8082" -ApiToken "change-me-token" +$BaseUrl = "http://127.0.0.1:8082" ``` -该脚本覆盖健康检查、存储状态、同步状态、记录、统计、时间段、映射和照片列表等低风险接口。需要真实硬件、真实 AES 正文或真实 JPEG 文件的接口仍按下方手动命令验证。 - -## 1. 测试端口和服务健康 +默认 Token: ```powershell -curl.exe -i --max-time 8 "http://127.0.0.1:8082/api/v1/system/health" +$ApiToken = "atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV" ``` -预期能看到 `HTTP/1.1 200 OK` 或类似成功响应。 - -## 2. 获取服务器时间 +建议先在 PowerShell 中设置: ```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/system/time" +$BaseUrl = "http://127.0.0.1:8082" +$ApiToken = "atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV" +$Headers = @{ + "X-Api-Token" = $ApiToken + "Accept" = "application/json" +} ``` -## 2.0 查看 SQLite 存储状态 +如果服务端运行在公网 IP,例如 `27.106.113.205`: ```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/system/storage" +$BaseUrl = "http://27.106.113.205:8082" ``` -重点看: +## 2. 自动化回归脚本 -```text -"storage":"sqlite" -"database":"attendance.db" -"ok":true -"quick_check":"ok" -"records": 考勤记录数量 -"face_photos": 服务端照片数量 -"pending_deletes": K210待删除任务数量 -``` - -## 2.0.1 查看当前同步版本 +项目已经提供基础回归脚本: ```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/sync/version" +cd D:\University\graduation\code\Reconstruct_Qt\server +.\regression_api_tests.ps1 -BaseUrl "http://127.0.0.1:8082" -ApiToken "atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV" ``` -## 2.1 统一同步状态:K210 +脚本会检查: + +1. 健康检查。 +2. 存储状态。 +3. 时间接口。 +4. 同步版本。 +5. K210/STM32 同步状态。 +6. 记录和统计。 +7. 时间段。 +8. 查重。 +9. 今日记录。 +10. 映射。 +11. 照片。 +12. 学生 ID。 +13. 待删除任务。 +14. 同步请求。 + +## 3. 基础连通性 + +### 3.1 健康检查 ```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/sync/state?client=k210&last_version=0" +Invoke-RestMethod -Uri "$BaseUrl/api/v1/system/health" ``` -重点看: +不需要 Token。正常应返回 `status = healthy`。 -```text -"version": 当前服务端同步版本 -"changed": true/false -"face_mappings": 人脸ID到学号映射 -"pending_delete": K210需要删除的本地人脸ID -``` - -## 2.2 统一同步状态:STM32 +### 3.2 带 Token 健康检查 ```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/sync/state?client=stm32&last_version=0" +Invoke-RestMethod -Uri "$BaseUrl/api/v1/system/health" -Headers $Headers ``` -重点看 `time_slots` 是否正确返回。 - -## 2.3 统一同步状态:Qt +### 3.3 端口连通 ```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/sync/state?client=qt&last_version=0" +Test-NetConnection -ComputerName 127.0.0.1 -Port 8082 ``` -## 2.4 K210 确认统一同步删除 - -把 `2` 换成真实已删除成功的人脸 ID。 +公网服务器: ```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" -H "Content-Type: application/json" -X POST --data-binary "{\"client\":\"k210\",\"version\":1,\"deleted\":[\"2\"]}" "http://127.0.0.1:8082/api/v1/sync/ack" +Test-NetConnection -ComputerName 27.106.113.205 -Port 8082 ``` -## 3. 获取全部考勤记录 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/attendance/records?limit=1000&offset=0" -``` - -后端当前以 `attendance.db` 为准。旧 `attendance_database.csv` 只会在首次启动 SQLite 时作为迁移来源,之后新增记录不会再写回 CSV。 - -如果本机安装了 `sqlite3.exe`,也可以在服务端运行目录执行: - -```powershell -sqlite3.exe attendance.db "SELECT timestamp,student_id,name,confidence,status,class_id FROM records ORDER BY id DESC LIMIT 10;" -``` - -备份当前数据库: - -```powershell -sqlite3.exe attendance.db ".backup attendance_backup.db" -``` - -## 4. 获取统计信息 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/attendance/statistics" -``` - -## 5. 获取签到时间段 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/config/time-slots" -``` - -## 6. 保存签到时间段 - -用于测试 Qt 端 `/api/v1/config/time-slots` 保存失败的问题。执行后观察后端日志,正常应该显示 `POST body size` 大于 0。 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" -H "Content-Type: application/json" -X POST --data-binary "{\"time_slots\":[{\"name\":\"上午签到\",\"start_time\":\"08:00\",\"end_time\":\"09:30\",\"enabled\":true},{\"name\":\"中午签退\",\"start_time\":\"12:00\",\"end_time\":\"13:00\",\"enabled\":true},{\"name\":\"下午签到\",\"start_time\":\"14:00\",\"end_time\":\"15:00\",\"enabled\":true},{\"name\":\"晚上签退\",\"start_time\":\"17:00\",\"end_time\":\"18:30\",\"enabled\":true}]}" "http://127.0.0.1:8082/api/v1/config/time-slots" -``` - -## 7. 检查是否重复签到 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/attendance/duplicates?student_id=1&slot_start=00:00&slot_end=23:59" -``` - -## 8. 获取今天指定时间段记录 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/attendance/today?slot_start=00:00&slot_end=23:59" -``` - -也可以测试指定签到时间段: - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/attendance/today?slot_start=14:00&slot_end=15:00" -``` - -## 9. 获取映射配置 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/config/mappings" -``` - -## 10. 保存映射配置 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" -H "Content-Type: application/json" -X POST --data-binary "{\"student_name_map\":{\"1\":\"ID1\"},\"class_name_map\":{\"1\":\"一班\"}}" "http://127.0.0.1:8082/api/v1/config/mappings" -``` - -## 11. 获取人脸照片列表 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/faces/photos" -``` - -## 12. 下载指定人脸照片 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" "http://127.0.0.1:8082/api/v1/faces/photos/1" --output "%TEMP%\backend_face_1.jpg" -``` - -如果 `face_id=1` 存在,照片会保存到: - -```text -%TEMP%\backend_face_1.jpg -``` - -## 13. 上传人脸照片 - -把 `C:\path\to\face.jpg` 换成真实 jpg 文件路径。 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Content-Type: image/jpeg" --data-binary "@C:\path\to\face.jpg" "http://127.0.0.1:8082/api/v1/faces/photos/1" -``` - -## 14. 获取人脸 ID 映射 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/faces/mappings" -``` - -## 15. 保存人脸 ID 映射 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" -H "Content-Type: application/json" -X POST --data-binary "{\"mappings\":{\"1\":\"1\"}}" "http://127.0.0.1:8082/api/v1/faces/mappings" -``` - -## 16. 获取全部学生 ID - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/faces/student-ids" -``` - -## 17. 获取待删除列表 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/faces/pending-delete" -``` - -## 18. 确认删除同步完成 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" -H "Content-Type: application/json" -X POST --data-binary "{\"face_id\":\"1\"}" "http://127.0.0.1:8082/api/v1/faces/delete-ack" -``` - -## 19. K210 查询是否需要同步 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" "http://127.0.0.1:8082/api/v1/sync/request" -``` - -## 20. 触发 K210 同步 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" -H "Content-Type: application/json" -X POST --data-binary "{\"sync\":true}" "http://127.0.0.1:8082/api/v1/sync/trigger" -``` - -## 21. K210 同步确认 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" -H "Content-Type: application/json" -X POST --data-binary "{\"ack\":true}" "http://127.0.0.1:8082/api/v1/sync/request/ack" -``` - -## 22. 加密签到接口 - -`/api/v1/attendance/checkins` 需要 STM32 生成的 AES-128-CBC Base64 加密正文。把 `BODY` 替换成真实加密数据。 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "X-Encrypted: AES128-CBC" -H "Content-Type: application/octet-stream" --data-binary "BODY" "http://127.0.0.1:8082/api/v1/attendance/checkins" -``` - -## 23. 删除单条考勤记录 - -危险操作,会删除真实数据。执行前把时间和学生 ID 改成要删除的记录。 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -X DELETE "http://127.0.0.1:8082/api/v1/attendance/records?timestamp=2026-05-01%2014%3A30%3A38&student_id=1" -``` - -## 24. 删除指定人脸 - -危险操作,只删除人脸信息,不删除考勤记录。 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -X DELETE "http://127.0.0.1:8082/api/v1/faces/1" -``` - -## 25. 删除指定人脸并删除相关考勤记录 - -危险操作,会删除人脸和相关记录。 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -X DELETE "http://127.0.0.1:8082/api/v1/faces/1?delete_records=true" -``` - -## 26. 清空全部考勤记录 - -危险操作,会清空考勤数据库。 - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Content-Type: application/json" -X POST --data-binary "{}" "http://127.0.0.1:8082/api/v1/attendance/records/clear" -``` - -## 27. Windows 上检查端口占用 +### 3.4 检查端口占用 ```powershell netstat -ano | findstr :8082 ``` -如果能看到 `LISTENING`,说明本机有程序正在监听 `8082`。 +## 4. 系统接口 -## 28. Windows 上测试 TCP 端口连通 +### 4.1 获取服务器时间 ```powershell -Test-NetConnection 127.0.0.1 -Port 8082 +Invoke-RestMethod -Uri "$BaseUrl/api/v1/system/time" -Headers $Headers ``` -公网服务器测试: - -```powershell -Test-NetConnection 127.0.0.1 -Port 8082 -``` - -`TcpTestSucceeded : True` 表示 TCP 端口可连通。 - -## 29. 判断 Qt POST 是否真的发送了请求体 - -先手动执行这条: - -```powershell -curl.exe -i --max-time 8 -H "X-Api-Token: change-me-token" -H "Accept: application/json" -H "Content-Type: application/json" -X POST --data-binary "{\"time_slots\":[{\"name\":\"下午签到\",\"start_time\":\"14:00\",\"end_time\":\"15:00\",\"enabled\":true}]}" "http://127.0.0.1:8082/api/v1/config/time-slots" -``` - -如果后端日志显示: +返回应包含: ```text -[HTTP] POST body size: xx bytes +epoch +server_time ``` -说明服务端可以正常接收请求体。 +### 4.2 查看存储状态 -如果 Qt 保存时后端仍显示: +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/system/storage" -Headers $Headers +``` + +用于确认 SQLite、记录数、照片数、待删除任务等。 + +### 4.3 不带 Token 测试鉴权 + +```powershell +try { + Invoke-RestMethod -Uri "$BaseUrl/api/v1/system/storage" +} catch { + $_.Exception.Response.StatusCode.value__ +} +``` + +正常应该得到 `401`。 + +## 5. 同步接口 + +### 5.1 查看当前同步版本 + +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/sync/version" -Headers $Headers +``` + +### 5.2 Qt 同步状态 + +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/sync/state?client=qt&last_version=0" -Headers $Headers +``` + +返回内容包含 Qt 需要的数据,例如记录、统计、时间段、映射、照片和学生 ID。 + +### 5.3 STM32 同步状态 + +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/sync/state?client=stm32&last_version=0" -Headers $Headers +``` + +重点看: ```text -[HTTP] POST body size: 0 bytes +state.time_slots ``` -说明问题在 Qt 端发送 POST 的方式上。 +当前默认应为空数组,除非已经在 Qt 中保存过时间段。 + +### 5.4 K210 同步状态 + +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/sync/state?client=k210&last_version=0" -Headers $Headers +``` + +重点看: + +```text +state.face_mappings +state.pending_delete +state.delete_tasks +``` + +### 5.5 K210 同步 ACK + +```powershell +$Body = @{ + client = "k210" + version = 1 + deleted = @() + task_ids = @() +} + +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/sync/ack" ` + -Method POST ` + -Headers $Headers ` + -ContentType "application/json" ` + -Body ($Body | ConvertTo-Json -Compress) +``` + +## 6. 考勤记录接口 + +### 6.1 获取记录 + +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/attendance/records?limit=10&offset=0" -Headers $Headers +``` + +### 6.2 获取统计 + +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/attendance/statistics" -Headers $Headers +``` + +### 6.3 删除单条记录 + +先准备参数: + +```powershell +$Timestamp = [uri]::EscapeDataString("2026-05-01 08:10:00") +$StudentId = [uri]::EscapeDataString("20240001") +``` + +删除: + +```powershell +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/attendance/records?timestamp=$Timestamp&student_id=$StudentId" ` + -Method DELETE ` + -Headers $Headers +``` + +### 6.4 清空全部记录 + +危险操作,会删除全部考勤记录: + +```powershell +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/attendance/records/clear" ` + -Method POST ` + -Headers $Headers ` + -ContentType "application/json" +``` + +## 7. 时间段接口 + +### 7.1 获取时间段 + +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/config/time-slots" -Headers $Headers +``` + +新部署默认应为: + +```json +{ + "time_slots": [] +} +``` + +### 7.2 保存空时间段 + +```powershell +$Body = @{ + time_slots = @() +} + +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/config/time-slots" ` + -Method POST ` + -Headers $Headers ` + -ContentType "application/json" ` + -Body ($Body | ConvertTo-Json -Depth 5 -Compress) +``` + +### 7.3 保存一个时间段 + +```powershell +$Body = @{ + time_slots = @( + @{ + name = "上午签到" + start_time = "08:00" + end_time = "09:30" + enabled = $true + } + ) +} + +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/config/time-slots" ` + -Method POST ` + -Headers $Headers ` + -ContentType "application/json" ` + -Body ($Body | ConvertTo-Json -Depth 5 -Compress) +``` + +### 7.4 检查重复签到 + +```powershell +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/attendance/duplicates?student_id=20240001&slot_start=08:00&slot_end=09:30" ` + -Headers $Headers +``` + +### 7.5 获取今日某时间段记录 + +```powershell +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/attendance/today?slot_start=08:00&slot_end=09:30" ` + -Headers $Headers +``` + +## 8. 名称映射接口 + +### 8.1 获取映射 + +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/config/mappings" -Headers $Headers +``` + +### 8.2 保存映射 + +```powershell +$Body = @{ + student_name_map = @{ + "20240001" = "张三" + } + class_name_map = @{ + "1" = "软件工程一班" + } +} + +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/config/mappings" ` + -Method POST ` + -Headers $Headers ` + -ContentType "application/json" ` + -Body ($Body | ConvertTo-Json -Depth 10 -Compress) +``` + +## 9. 人脸照片和映射接口 + +### 9.1 获取照片列表 + +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/faces/photos" -Headers $Headers +``` + +### 9.2 下载照片 + +```powershell +$FaceId = "1" +Invoke-WebRequest ` + -Uri "$BaseUrl/api/v1/faces/photos/$FaceId" ` + -Headers $Headers ` + -OutFile ".\face_$FaceId.jpg" +``` + +如果服务端没有该照片,会返回 404。 + +### 9.3 上传测试照片 + +假设当前目录有 `test.jpg`: + +```powershell +$FaceId = "test001" +$PhotoBytes = [System.IO.File]::ReadAllBytes((Resolve-Path ".\test.jpg")) + +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/faces/photos/$FaceId" ` + -Method POST ` + -Headers $Headers ` + -ContentType "image/jpeg" ` + -Body $PhotoBytes +``` + +### 9.4 获取人脸映射 + +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/faces/mappings" -Headers $Headers +``` + +### 9.5 保存人脸映射 + +```powershell +$Body = @{ + mappings = @{ + "1" = "20240001" + } +} + +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/faces/mappings" ` + -Method POST ` + -Headers $Headers ` + -ContentType "application/json" ` + -Body ($Body | ConvertTo-Json -Depth 5 -Compress) +``` + +保存后应提升同步版本,并触发 K210 同步请求。 + +### 9.6 获取全部学生 ID + +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/faces/student-ids" -Headers $Headers +``` + +### 9.7 删除人脸 + +只删除人脸相关信息: + +```powershell +$StudentId = [uri]::EscapeDataString("20240001") +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/faces/$StudentId" ` + -Method DELETE ` + -Headers $Headers +``` + +删除人脸并删除该学生考勤记录: + +```powershell +$StudentId = [uri]::EscapeDataString("20240001") +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/faces/$StudentId?delete_records=true" ` + -Method DELETE ` + -Headers $Headers +``` + +## 10. K210 删除同步接口 + +### 10.1 获取待删除任务 + +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/faces/pending-delete" -Headers $Headers +``` + +返回可能包含: + +```text +pending_delete +delete_tasks +``` + +### 10.2 旧式删除确认 + +```powershell +$Body = @{ + face_id = "1" +} + +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/faces/delete-ack" ` + -Method POST ` + -Headers $Headers ` + -ContentType "application/json" ` + -Body ($Body | ConvertTo-Json -Compress) +``` + +### 10.3 查询同步请求标志 + +```powershell +Invoke-RestMethod -Uri "$BaseUrl/api/v1/sync/request" -Headers $Headers +``` + +### 10.4 确认同步请求 + +```powershell +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/sync/request/ack" ` + -Method POST ` + -Headers $Headers ` + -ContentType "application/json" ` + -Body '{"ack":true}' +``` + +### 10.5 触发 K210 同步 + +```powershell +Invoke-RestMethod ` + -Uri "$BaseUrl/api/v1/sync/trigger" ` + -Method POST ` + -Headers $Headers ` + -ContentType "application/json" +``` + +## 11. 加密签到接口说明 + +STM32 实际上传的是 AES-128-CBC 加密后的 Base64。手动构造比较麻烦,通常不建议用 PowerShell 直接手写。 + +服务端要求: + +```text +POST /api/v1/attendance/checkins +X-Api-Token: +X-Encrypted: AES128-CBC +``` + +明文格式应是: + +```json +{ + "student_id": "20240001", + "name": "张三", + "confidence": "0.95", + "status": "success", + "class_id": "1" +} +``` + +如果只是测试记录列表、统计、配置和同步,不需要手动构造加密签到。 + +## 12. 常见错误 + +### 12.1 401 Unauthorized + +原因:Token 缺失或不一致。 + +检查: + +```powershell +$Headers +``` + +### 12.2 404 Not Found + +原因: + +1. 路径写错。 +2. 服务端还没启动新版本。 +3. 照片 ID 不存在。 + +### 12.3 Invoke-RestMethod 连接失败 + +检查服务端是否启动: + +```powershell +Test-NetConnection -ComputerName 127.0.0.1 -Port 8082 +``` + +### 12.4 Qt 能访问,本机命令不能访问 + +可能 Qt 用的是公网地址,而 PowerShell `$BaseUrl` 还是 `127.0.0.1`。统一服务端地址。 + +### 12.5 STM32/K210 不能访问,电脑能访问 + +可能原因: + +1. STM32/K210 配置的是公网 IP,本机测的是本地地址。 +2. WiFi 与服务器不在同一网络。 +3. 路由器或服务器防火墙阻止端口。 +4. Token 写错。 diff --git a/server/server_encrypted.cpp b/server/server_encrypted.cpp index 75f1335..36b9df5 100644 --- a/server/server_encrypted.cpp +++ b/server/server_encrypted.cpp @@ -69,6 +69,8 @@ const size_t MAX_PHOTO_BODY_SIZE = 512 * 1024; const char* DEFAULT_API_TOKEN = "atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV"; const char* DEFAULT_CORS_ORIGIN = "http://localhost"; const char* DEFAULT_TIMESLOTS_JSON = + "{\"time_slots\":[]}"; +const char* LEGACY_DEFAULT_TIMESLOTS_JSON = "{\"time_slots\":[" "{\"name\":\"上午签到\",\"start_time\":\"08:00\",\"end_time\":\"09:30\",\"enabled\":true}," "{\"name\":\"中午签退\",\"start_time\":\"12:00\",\"end_time\":\"13:00\",\"enabled\":true}," @@ -139,6 +141,17 @@ std::string jsonEscape(const std::string& value) { return out.str(); } +std::string jsonWithoutWhitespace(const std::string& value) { + std::string compact; + compact.reserve(value.size()); + for (char ch : value) { + if (!std::isspace(static_cast(ch))) { + compact.push_back(ch); + } + } + return compact; +} + std::string csvEscape(const std::string& value) { bool needs_quotes = false; for (char c : value) { @@ -1300,6 +1313,11 @@ bool saveTimeSlots(const std::string& json_data) { std::string readTimeSlots() { std::string content = sqliteGetKvValue("time_slots", DEFAULT_TIMESLOTS_JSON); + if (jsonWithoutWhitespace(content) == jsonWithoutWhitespace(LEGACY_DEFAULT_TIMESLOTS_JSON)) { + content = DEFAULT_TIMESLOTS_JSON; + sqliteSetKvValue("time_slots", content); + std::cout << "[TIMESLOTS] Legacy default configuration cleared" << std::endl; + } std::cout << "[TIMESLOTS] Read " << content.size() << " bytes from SQLite" << std::endl; return content; }