更新文档

This commit is contained in:
2026-05-01 23:03:42 +08:00
parent 5a31ff2179
commit a32baa96e3
14 changed files with 2799 additions and 1853 deletions
+6 -14
View File
@@ -23,15 +23,6 @@ static int8_t cached_time_slots[MAX_CACHED_IDS];
static uint8_t cached_id_count = 0; static uint8_t cached_id_count = 0;
static int8_t active_cache_slot = -1; 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读取) // 当前时间(由外部更新,如果有RTC则从RTC读取)
static uint8_t current_hour = 8; static uint8_t current_hour = 8;
static uint8_t current_minute = 30; static uint8_t current_minute = 30;
@@ -74,9 +65,9 @@ void Attendance_Init(void)
current_minute = 0; current_minute = 0;
memset(attendance_records, 0, sizeof(attendance_records)); memset(attendance_records, 0, sizeof(attendance_records));
// 初始化为默认时间段配置 // 启动时不预置考勤时段,等待服务端或上位机下发配置。
memcpy(time_slots, default_time_slots, sizeof(default_time_slots)); memset(time_slots, 0, sizeof(time_slots));
time_slot_count = DEFAULT_TIME_SLOT_COUNT; time_slot_count = 0;
// 清空缓存 // 清空缓存
cached_id_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++; while (*ptr == ' ' || *ptr == '\n' || *ptr == '\r' || *ptr == '\t') ptr++;
if (*ptr == ']') { if (*ptr == ']') {
DEBUG_INFO("[Attendance] Empty time slot config, keeping current slots"); Attendance_UpdateTimeSlots(temp_slots, 0);
return time_slot_count; DEBUG_INFO("[Attendance] Empty time slot config applied");
return 0;
} }
// 解析每个时间段对象 // 解析每个时间段对象
+114 -474
View File
@@ -1,517 +1,157 @@
# 人脸识别考勤系统 # Qt 管理端 README
基于 Qt 6 Quick/QML 开发的跨平台人脸识别考勤管理客户端,用于连接考勤服务端,完成签到记录查看、筛选导出、统计分析、签到时段配置、学生与班级名称映射、人脸照片与学号绑定等管理工作。 这是人脸识别考勤系统的 Qt 管理端,支持 Windows 桌面端和 Android 端。更完整的说明见:
本项目是纪田野的个人毕业设计作品,主要用于人脸识别考勤场景的功能展示、系统联调和学习研究。 ```text
../docs/04_Qt管理端文档.md
../docs/00_项目总文档.md
```
## 项目特点 ## 1. 功能概览
- 跨平台客户端:支持 Windows 桌面端和 Android 端构建。 Qt 管理端用于连接考勤服务端,完成以下工作:
- 单窗口管理:考勤记录、统计分析、配置中心、人脸管理和系统说明集中在一个界面中。
- 响应式界面:针对手机窄屏做了独立布局适配,避免控件重叠和文字遮挡。
- 数据导出:支持将考勤记录和统计报表导出为 CSV 文件。
- 本地配置缓存:服务端地址、签到时段、映射配置会保存到本地。
- 自动重连:服务端不可达时自动重连,并使用简短状态提示。
- Android 字体优化:Android 端使用原生文字渲染,并降低粗体权重,减少中文粗体重影。
- Android 路径兼容:Gradle 配置已允许中文路径构建,并启用 AndroidX。
## 技术栈 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>
```
默认 Token 写在 `src/ApiClient.cpp`。可以通过环境变量覆盖:
```powershell
$env:ATTENDANCE_API_TOKEN="你的Token"
```
### 2.4 响应加密
服务端支持通过请求头 `X-Encrypted-Response: true` 返回 AES 加密响应,Qt 代码中保留了解密能力。
当前普通记录和统计请求没有默认开启这个请求头,实际按 JSON 明文解析。
## 3. 技术栈
| 分类 | 内容 | | 分类 | 内容 |
| --- | --- | | --- | --- |
| 开发语言 | C++17、QML | | 语言 | C++17、QML |
| UI 框架 | Qt Quick / Qt Quick Controls 2 | | UI | Qt Quick / Qt Quick Controls 2 |
| Qt 模块 | Core、Gui、Qml、Quick、QuickControls2、Network | | Qt 模块 | Core、Gui、Qml、Quick、QuickControls2、Network |
| 构建系统 | CMake 3.16+ | | 构建 | CMake 3.16+ |
| 桌面编译器 | MinGW / MSVC / Clang / GCC | | 平台 | Windows 桌面端、Android |
| Android 构建 | Qt for Android、Android SDK、Android NDK、JDK |
| 加密处理 | AES-128-CBC,代码位于 `src/CryptoUtils.cpp` |
## 目录结构 ## 4. 目录结构
```text ```text
rewrite_qt/ QT/
├── android/ # Android 打包补充资源和 Gradle 配置 ├── android/ # Android 打包配置和图标
│ ├── gradle.properties ├── assets/ # 桌面端图标
│ └── res/mipmap-*/ # Android 启动图标 ├── cmake/ # 平台模板
├── assets/ # 桌面端图标资源 ├── qml/Main.qml # 主界面
├── cmake/ # 平台相关模板文件
├── qml/
│ └── Main.qml # 主界面
├── src/ ├── src/
│ ├── ApiClient.* # 服务端 API 客户端 │ ├── ApiClient.* # HTTP API 客户端
│ ├── AppController.* # QML 与业务逻辑中枢 │ ├── AppController.* # 业务逻辑中枢
│ ├── AttendanceTypes.h # 考勤数据结构 │ ├── AttendanceTypes.h # 数据结构
│ ├── CryptoUtils.* # AES 解密工具 │ ├── CryptoUtils.* # AES 解密兼容工具
│ ├── *ListModel.* # 暴露给 QML 的列表模型 │ └── *ListModel.* # QML 列表模型
│ └── crypto/tiny_aes.hpp # AES 底层实现
├── CMakeLists.txt ├── CMakeLists.txt
├── LICENSE ├── LICENSE
├── README.md ├── README.md
└── main.cpp └── main.cpp
``` ```
## 功能说明 ## 5. 构建环境
### 考勤记录 基础要求:
考勤记录页用于查看和管理服务端同步下来的签到记录。 1. Qt 6.2 或更高。
2. CMake 3.16 或更高。
3. 支持 C++17 的编译器。
- 查看总记录数、今日签到数、当前筛选结果数。 Windows 推荐使用 Qt 6.x MinGW 64-bit Kit。
- 按班级、学号、姓名关键字筛选记录。
- 按全部时间、今天、最近 7 天、最近 30 天、指定日期筛选。
- 刷新最新记录。
- 导出当前筛选结果为 CSV。
- 清空服务端全部考勤记录。
导出字段: Android 还需要:
```text 1. Qt for Android。
签到时间,班级编号,班级名称,学号,姓名,置信度,状态 2. Android SDK。
``` 3. Android NDK。
4. JDK。
5. Android 平台版本,例如 `android-35`。
### 统计分析 ## 6. Qt Creator 构建
统计分析页基于当前加载的考勤记录在本地计算统计结果。
- 支持今天、最近 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 构建
1. 打开 Qt Creator。 1. 打开 Qt Creator。
2. 选择项目根目录下的 `CMakeLists.txt`。 2. 打开 `QT/CMakeLists.txt`。
3. 选择对应 Kit,例如 Desktop Qt 6.x 或 Qt for Android。 3. 选择 Desktop Qt 或 Android Kit。
4. 等待 CMake 配置完成。 4. 等待 CMake 配置完成。
5. 构建目标 `apprewrite_qt`。 5. 构建目标 `apprewrite_qt`。
6. 桌面端可直接运行;Android 端可部署到设备或模拟器。 6. 桌面端直接运行;Android 端部署到设备或模拟器。
## 命令行构建 ## 7. 命令行构建示例
Windows 示例: Windows 示例:
```powershell ```powershell
cmake -S . -B build -DCMAKE_PREFIX_PATH=D:/Qt/6.10.0/mingw_64 cd D:\University\graduation\code\Reconstruct_Qt\QT
cmake --build build --target apprewrite_qt cmake -S . -B build\Desktop -DCMAKE_PREFIX_PATH=D:\Qt\6.10.0\mingw_64
cmake --build build\Desktop
``` ```
Linux 示例: Qt 安装路径需要按本机实际情况修改。
```bash ## 8. 本地数据
cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x/gcc_64
cmake --build build --target apprewrite_qt
```
安装到系统路径: 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 | | 服务端地址 | `QSettings`,键名 `server/url` |
| `mipmap-hdpi` | 72x72 | | 时间段配置 | `QStandardPaths::AppDataLocation/timeslot_config.json` |
| `mipmap-xhdpi` | 96x96 | | 学生/班级映射 | `QStandardPaths::AppDataLocation/mapping_config.json` |
| `mipmap-xxhdpi` | 144x144 | | 人脸照片缓存 | `QStandardPaths::AppDataLocation/face_photos/*.jpg` |
| `mipmap-xxxhdpi` | 192x192 |
## 界面与移动端适配 ## 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` 文件。
+22 -21
View File
@@ -7,8 +7,8 @@ ApplicationWindow {
id: window id: window
width: 1560 width: 1560
height: 920 height: 920
minimumWidth: 900 minimumWidth: Qt.platform.os === "android" ? 360 : 900
minimumHeight: 620 minimumHeight: Qt.platform.os === "android" ? 560 : 620
visible: true visible: true
title: "人脸识别考勤系统" title: "人脸识别考勤系统"
color: "#f6f7f9" color: "#f6f7f9"
@@ -41,11 +41,12 @@ ApplicationWindow {
property int currentPageIndex: 0 property int currentPageIndex: 0
property bool timeSlotsExpanded: false property bool timeSlotsExpanded: false
property bool mappingsExpanded: false property bool mappingsExpanded: false
readonly property bool androidLayout: Qt.platform.os === "android"
readonly property bool compactLayout: width < 1460 readonly property bool compactLayout: width < 1460
readonly property bool denseLayout: width < 1380 readonly property bool denseLayout: width < 1380
readonly property bool narrowLayout: width < 1280 readonly property bool narrowLayout: width < 1280
readonly property bool slimLayout: width < 1040 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) readonly property int sidebarWidth: slimLayout ? 0 : (compactLayout ? 212 : 240)
font.family: appFontFamily font.family: appFontFamily
@@ -169,12 +170,14 @@ ApplicationWindow {
} }
contentItem: Label { contentItem: Label {
width: control.availableWidth
text: control.text text: control.text
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: control.labelColor color: control.labelColor
font.pixelSize: 13 font.pixelSize: 13
font.weight: appBoldWeight font.weight: appBoldWeight
elide: Text.ElideRight
} }
} }
@@ -194,12 +197,14 @@ ApplicationWindow {
} }
contentItem: Label { contentItem: Label {
width: control.availableWidth
text: control.text text: control.text
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: control.labelColor color: control.labelColor
font.pixelSize: 13 font.pixelSize: 13
font.weight: appBoldWeight font.weight: appBoldWeight
elide: Text.ElideRight
} }
} }
@@ -1972,7 +1977,7 @@ ApplicationWindow {
radius: 8 radius: 8
color: panelStrong color: panelStrong
border.color: lineColor border.color: lineColor
Layout.preferredHeight: phoneLayout ? 218 : (slimLayout ? 174 : 88) Layout.preferredHeight: serverConfigGrid.implicitHeight + (slimLayout ? 24 : 36)
clip: true clip: true
GridLayout { GridLayout {
@@ -1998,13 +2003,13 @@ ApplicationWindow {
} }
SecondaryButton { SecondaryButton {
Layout.fillWidth: phoneLayout Layout.fillWidth: slimLayout
text: "保存地址" text: "保存地址"
onClicked: appController.serverUrl = serverUrlField.text onClicked: appController.serverUrl = serverUrlField.text
} }
PrimaryButton { PrimaryButton {
Layout.fillWidth: phoneLayout Layout.fillWidth: slimLayout
text: "测试连接" text: "测试连接"
fillColor: tealColor fillColor: tealColor
onClicked: appController.testConnection() onClicked: appController.testConnection()
@@ -2017,19 +2022,17 @@ ApplicationWindow {
radius: 8 radius: 8
color: panelStrong color: panelStrong
border.color: lineColor border.color: lineColor
Layout.preferredHeight: timeSlotsExpanded Layout.preferredHeight: timeSlotCardLayout.implicitHeight + 36
? 18 + (phoneLayout ? 132 : 64) + 12 + (phoneLayout ? 0 : 42) + (phoneLayout ? 0 : 12) + Math.max(phoneLayout ? 96 : 52, timeSlotListView.contentHeight) + 18
: (phoneLayout ? 124 : 100)
clip: true clip: true
ColumnLayout { ColumnLayout {
id: timeSlotCardLayout
anchors.fill: parent anchors.fill: parent
anchors.margins: 18 anchors.margins: 18
spacing: 12 spacing: 12
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: phoneLayout ? 132 : 64
spacing: 12 spacing: 12
SectionHeader { SectionHeader {
@@ -2040,7 +2043,7 @@ ApplicationWindow {
} }
GridLayout { GridLayout {
Layout.fillWidth: true Layout.fillWidth: true
columns: phoneLayout ? 3 : 5 columns: slimLayout ? 3 : 5
columnSpacing: 8 columnSpacing: 8
rowSpacing: 8 rowSpacing: 8
@@ -2058,7 +2061,7 @@ ApplicationWindow {
SecondaryButton { SecondaryButton {
Layout.fillWidth: true Layout.fillWidth: true
visible: timeSlotsExpanded visible: timeSlotsExpanded
text: "恢复默认" text: "清空时段"
onClicked: { onClicked: {
appController.timeSlotModel.resetToDefault(); appController.timeSlotModel.resetToDefault();
appController.saveTimeSlots(); appController.saveTimeSlots();
@@ -2067,13 +2070,13 @@ ApplicationWindow {
SecondaryButton { SecondaryButton {
Layout.fillWidth: true Layout.fillWidth: true
visible: timeSlotsExpanded visible: timeSlotsExpanded
text: phoneLayout ? "同步" : "从服务器同步" text: slimLayout ? "同步" : "从服务器同步"
onClicked: appController.reloadTimeSlotsFromServer() onClicked: appController.reloadTimeSlotsFromServer()
} }
PrimaryButton { PrimaryButton {
Layout.fillWidth: true Layout.fillWidth: true
visible: timeSlotsExpanded visible: timeSlotsExpanded
text: phoneLayout ? "保存" : "保存时间段" text: slimLayout ? "保存" : "保存时间段"
onClicked: appController.saveTimeSlots() onClicked: appController.saveTimeSlots()
} }
} }
@@ -2120,7 +2123,7 @@ ApplicationWindow {
id: timeSlotListView id: timeSlotListView
visible: timeSlotsExpanded visible: timeSlotsExpanded
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: visible ? contentHeight : 0 Layout.preferredHeight: visible ? Math.max(phoneLayout ? 96 : 52, contentHeight) : 0
clip: false clip: false
interactive: false interactive: false
spacing: 8 spacing: 8
@@ -2253,19 +2256,17 @@ ApplicationWindow {
radius: 8 radius: 8
color: panelStrong color: panelStrong
border.color: lineColor border.color: lineColor
Layout.preferredHeight: mappingsExpanded Layout.preferredHeight: mappingCardLayout.implicitHeight + 36
? 18 + (phoneLayout ? 120 : 64) + 12 + mappingTabs.implicitHeight + 12 + Math.max(phoneLayout ? 112 : 64, mappingTabs.currentIndex === 0 ? studentMappingListView.contentHeight : classMappingListView.contentHeight) + 18
: (phoneLayout ? 124 : 100)
clip: true clip: true
ColumnLayout { ColumnLayout {
id: mappingCardLayout
anchors.fill: parent anchors.fill: parent
anchors.margins: 18 anchors.margins: 18
spacing: 12 spacing: 12
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: phoneLayout ? 120 : 64
spacing: 12 spacing: 12
SectionHeader { SectionHeader {
Layout.fillWidth: true Layout.fillWidth: true
@@ -2324,7 +2325,7 @@ ApplicationWindow {
StackLayout { StackLayout {
visible: mappingsExpanded visible: mappingsExpanded
Layout.fillWidth: true 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 currentIndex: mappingTabs.currentIndex
Rectangle { Rectangle {
@@ -2861,7 +2862,7 @@ ApplicationWindow {
TextArea { TextArea {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: 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 readOnly: true
selectByMouse: true selectByMouse: true
selectByKeyboard: true selectByKeyboard: true
+52 -7
View File
@@ -79,6 +79,35 @@ QString safeFileName(QString value)
return value.isEmpty() ? QStringLiteral("unknown") : value; return value.isEmpty() ? QStringLiteral("unknown") : value;
} }
QVector<TimeSlot> 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<TimeSlot> &timeSlots)
{
const QVector<TimeSlot> 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) AppController::AppController(QObject *parent)
@@ -131,11 +160,11 @@ AppController::AppController(QObject *parent)
}); });
connect(&m_apiClient, &ApiClient::timeSlotsFetched, this, [this](const QVector<TimeSlot> &timeSlots) { connect(&m_apiClient, &ApiClient::timeSlotsFetched, this, [this](const QVector<TimeSlot> &timeSlots) {
if (!timeSlots.isEmpty()) {
m_timeSlotModel.setTimeSlots(timeSlots); m_timeSlotModel.setTimeSlots(timeSlots);
saveTimeSlotsToLocal(); saveTimeSlotsToLocal();
setStatusMessage(QStringLiteral("已从服务器同步时间段")); setStatusMessage(timeSlots.isEmpty()
} ? QStringLiteral("已从服务器同步空时间段配置")
: QStringLiteral("已从服务器同步时间段"));
}); });
connect(&m_apiClient, &ApiClient::timeSlotsUploaded, this, [this](bool success, const QString &message) { connect(&m_apiClient, &ApiClient::timeSlotsUploaded, this, [this](bool success, const QString &message) {
@@ -425,8 +454,14 @@ void AppController::saveFaceMappings()
QMap<QString, QString> mapping; QMap<QString, QString> mapping;
const QVector<FaceListModel::Item> items = m_faceModel.items(); const QVector<FaceListModel::Item> items = m_faceModel.items();
for (const FaceListModel::Item &item : items) { for (const FaceListModel::Item &item : items) {
if (!item.studentId.trimmed().isEmpty()) { const QString faceId = item.faceId.trimmed();
mapping[item.faceId] = item.studentId.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; m_faceIdMapping = mapping;
@@ -537,7 +572,7 @@ void AppController::loadTimeSlotsFromLocal()
{ {
QFile file(localSettingsPath("timeslot_config.json")); QFile file(localSettingsPath("timeslot_config.json"));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
m_timeSlotModel.setTimeSlots(defaultTimeSlots()); m_timeSlotModel.setTimeSlots({});
return; 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 QString AppController::localSettingsPath(const QString &fileName) const
@@ -725,6 +767,9 @@ void AppController::mergeFaceItems()
for (const QString &id : m_allStudentIds) { for (const QString &id : m_allStudentIds) {
allIds.insert(id); allIds.insert(id);
} }
for (auto it = m_faceIdMapping.cbegin(); it != m_faceIdMapping.cend(); ++it) {
allIds.insert(it.key());
}
QStringList sortedIds = allIds.values(); QStringList sortedIds = allIds.values();
std::sort(sortedIds.begin(), sortedIds.end()); std::sort(sortedIds.begin(), sortedIds.end());
+1 -6
View File
@@ -42,12 +42,7 @@ struct TimeSlot
inline QVector<TimeSlot> defaultTimeSlots() inline QVector<TimeSlot> defaultTimeSlots()
{ {
return { 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}
};
} }
Q_DECLARE_METATYPE(AttendanceRecord) Q_DECLARE_METATYPE(AttendanceRecord)
+1 -1
View File
@@ -423,7 +423,7 @@ uint8_t DownloadTimeSlots(void)
// 解析JSON并更新时间段配置 // 解析JSON并更新时间段配置
uint8_t slot_count = Attendance_ParseTimeSlotsJSON(response_buffer); 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); DEBUG_PRINT("Time slot configuration updated: %d slots\r\n", slot_count);
if (server_version > 0) { if (server_version > 0) {
g_stm32_sync_version = server_version; g_stm32_sync_version = server_version;
+348 -40
View File
@@ -1,49 +1,357 @@
# 项目总文档 # 人脸识别考勤系统项目总文档
## 1. 项目概览 ## 1. 这是什么项目
本项目是“人脸识别考勤系统”多端协同工程,包含:
- STM32 固件端(设备控制与签到逻辑)
- K210 识别端(视觉识别与人脸管理)
- C++ 服务端(加密签到、数据管理、同步控制)
- Qt 管理端(后台管理与可视化)
## 2. 推荐运行链路 本项目是一个多端协同的人脸识别考勤系统。它不是只有一个程序,而是由四个主要部分组成:
建议使用以下版本组合:
1. 服务端:`server/server_encrypted.cpp`(HTTP 8082) | 部分 | 所在目录 | 主要职责 | 可以把它理解成 |
2. STM32:`User/main.c`(加密上传 + 时间段同步) | --- | --- | --- | --- |
3. K210:`k210/main.py`(识别 + 照片上传 + 同步) | STM32 固件端 | `User/`、`Hardware/`、`System/`、`Library/`、`Start/` | 接收 K210 识别结果、判断是否允许签到、加密上传记录、控制 OLED/蜂鸣器/指示灯/补光灯 | 考勤机主控板 |
4. Qt:`QT/CMakeLists.txt` 构建的客户端 | 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. 总体架构 ## 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. 文档索引(分端) ```mermaid
- STM32 固件端:`docs/01_STM32固件端文档.md` flowchart LR
- K210 识别端:`docs/02_K210识别端文档.md` Camera["摄像头"] --> K210["K210 识别端"]
- 服务端:`docs/03_服务端文档.md` K210 -->|"UART1: ID, NAME, CONF"| STM32["STM32 主控"]
- Qt 管理端:`docs/04_Qt管理端文档.md` STM32 -->|"USART1 AT 指令"| ESP1["ESP8266"]
- 项目审查问题清单:`docs/05_项目审查问题清单.md` 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. 各端关键配置位置 ## 4. 数据流说明
- STM32 网络与班级配置:`User/main.c`
- K210 WiFi/服务器配置:`k210/main.py`
- Qt 默认服务地址与 API 封装:`QT/src/ApiClient.cpp`、`QT/src/AppController.cpp`
- 服务端端口与数据文件:`server/server_encrypted.cpp`
## 6. 一致性要求 ### 4.1 正常签到流程
- AES Key/IV 必须在 STM32、服务端、Qt 三端一致。
- 服务端建议统一使用加密版本,避免接口行为不一致。
- 各端时间配置需统一时区(当前代码以 UTC+8 逻辑处理)。
## 7. 快速部署顺序 1. K210 摄像头采集画面。
1. 编译并启动服务端(确认 `:8082` 可访问)。 2. K210 使用三个 KPU 模型执行人脸检测、关键点定位和特征提取。
2. 启动 Qt,先做 `/api/v1/system/health` 连通性验证。 3. K210 将当前人脸特征与 SD 卡中的 `recordftr.txt` 做比对。
3. 烧录 STM32 固件并联网。 4. 如果分数大于阈值,K210 认为识别成功。
4. 启动 K210 脚本并确认识别/同步正常。 5. K210 通过 UART1 发送给 STM32:
5. 用 Qt 检查签到记录、照片、映射与删除同步链路。
```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/<face_id>.jpg`。
5. K210 将照片 ID 放入上传队列。
6. 网络任务空闲时,K210 通过 ESP8266 上传照片到服务端:
```text
POST /api/v1/faces/photos/<face_id>
```
7. Qt 管理端进入“人脸管理”,刷新后可以看到照片。
8. 管理员为该 face_id 填写真实学号并保存映射。
9. 服务端保存映射并触发同步。
10. K210 拉取同步状态后,把本地人脸名称从 `ID:<face_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 下手动测试和回归检查命令 |
+324 -53
View File
@@ -1,66 +1,337 @@
# STM32 固件端文档 # STM32 固件端文档
## 1. 端侧职责 ## 1. STM32 在系统中做什么
- 从 K210 串口接收识别结果。
- 在本地执行签到逻辑(时间段判断、重复判定、缓存与重传)。
- 通过 ESP8266 将签到数据以 AES-128-CBC 加密后上传服务端。
- 维护 RTC(DS1302)与网络时间同步。
对应入口文件:`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. 当前默认配置(代码内) 1. 初始化 OLED、蜂鸣器、LED、串口、RTC、光敏传感器、补光灯等硬件。
文件:`User/main.c` 2. 接收 K210 通过 USART3 发来的识别结果。
- `WIFI_SSID`: `YOUR_WIFI_SSID` 3. 判断识别置信度是否足够。
- `WIFI_PASSWORD`: `YOUR_WIFI_PASSWORD` 4. 判断当前时间是否在服务端配置的签到时间段内。
- `HTTP_SERVER_IP`: `127.0.0.1`(部署时改为实际服务端地址) 5. 判断当前学生在本时间段是否已经签过。
- `HTTP_SERVER_PORT`: `8082` 6. 生成签到记录。
- `HTTP_API_TOKEN`: `change-me-token`(部署时必须修改) 7. 使用 AES-128-CBC 加密签到 JSON。
- `CLASS_ID`: `1` 8. 通过 ESP8266 发送 HTTP 请求到服务端。
9. 网络异常时把记录留在本地,后续自动重试。
建议将上述配置改为部署环境参数后再编译烧录。 ## 2. 相关文件
## 4. 通信与接口 | 文件 | 作用 |
STM32 通过 `ESP8266_HttpGet` / `ESP8266_SendEncryptedData` 访问服务端: | --- | --- |
- `POST /api/v1/attendance/checkins`:上传加密签到数据(必带 `X-Encrypted: AES128-CBC`)。 | `User/main.c` | 主程序入口、初始化流程、主循环、联网和同步调度 |
- `GET /api/v1/system/time`:获取服务器时间戳。 | `Hardware/Attendance.c` | 考勤业务逻辑:时间段、重复判断、本地记录、同步状态 |
- `GET /api/v1/sync/state?client=stm32&last_version=<version>`:按版本同步时间段配置。 | `Hardware/Attendance.h` | 考勤数据结构和函数声明 |
- `GET /api/v1/attendance/duplicates`:按时段查重。 | `Hardware/K210_USART.c` | 解析 K210 串口识别结果 |
- `GET /api/v1/attendance/today`:下载今日已签到学号列表。 | `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. 主流程说明 ## 3. 硬件连接
1. 系统初始化(OLED、串口、考勤模块、加密模块、RTC、传感器)。
2. 初始化并连接 ESP8266 到 WiFi 与 HTTP 服务器。
3. 可用时同步网络时间到 DS1302。
4. 定时拉取时间段配置。
5. 处理 K210 上报的人脸识别结果:
- 置信度通过阈值后进入签到逻辑。
- 本地去重 + 服务器时段去重。
- 记录加入本地队列并尝试上传。
6. 网络异常时离线缓存,恢复后批量补传。
## 6. 调试建议 | 外设 | STM32 引脚 | 说明 |
- 优先看串口调试输出(`DEBUG_PRINT` 系列)。 | --- | --- | --- |
- 检查 `g_wifi_connected`、`g_server_connected` 变化。 | ESP8266 TX/RX | PA10 / PA9 | USART1,波特率 115200 |
- 若频繁重连,先确认 AP 稳定性、服务器可达性与 8082 端口。 | 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. 构建与烧录 ## 4. 代码中的主要配置
工程为 Keil 工程:
- `Project.uvprojx`
- `Project.uvoptx`
按当前板卡配置在 Keil 中编译并烧录。 位置:`User/main.c`
## 8. 依赖与注意事项 | 配置 | 当前值 | 说明 |
- 与服务端 AES Key/IV 必须一致(否则 `/api/v1/attendance/checkins` 解密失败)。 | --- | --- | --- |
- DS1302 可用时优先作为本地时钟;不可用则回退网络时间。 | `WIFI_SSID` | `yzfh` | ESP8266 要连接的 WiFi |
- `Attendance.c` 的默认时间段仅为兜底,实际以服务器下发为准。 | `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: <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` |
+279 -61
View File
@@ -1,75 +1,293 @@
# K210 识别端文档 # K210 识别端文档
## 1. 端侧职责 ## 1. K210 在系统中做什么
- 进行人脸检测、特征提取、识别。
- 通过 UART1 将识别结果实时发送给 STM32。
- 通过 UART2 驱动 ESP8266,与服务端同步人脸映射与删除任务。
- 上传新增人脸照片到服务端。
对应入口文件:`k210/main.py` K210 负责“看见人脸并判断是谁”。它不直接决定能不能签到,最终签到判断由 STM32 完成。
## 2. 通信链路 K210 的职责包括:
- `UART1`(K210 <-> STM32):发送识别结果。
- `UART2`(K210 <-> ESP8266):HTTP 通信到服务端 `:8082`。
识别结果发送格式(给 STM32): 1. 初始化摄像头和 LCD。
- 已识别:`ID:<id>,NAME:<name>,CONF:<score>` 2. 加载人脸检测、关键点检测、特征提取三个 KPU 模型。
- 陌生人:`UNKNOWN` 3. 从 SD 卡读取已经录入的人脸特征。
4. 实时检测画面中的人脸。
5. 将识别结果通过 UART1 发给 STM32。
6. 通过按键添加或删除本地人脸。
7. 保存新增人脸照片到 SD 卡。
8. 通过 K210 侧 ESP8266 上传照片到服务端。
9. 从服务端同步人脸 ID 到学号的映射。
10. 从服务端同步待删除任务,并删除本地特征。
## 3. 本地数据文件 ## 2. 相关文件
- 人脸特征库:`/sd/recordftr.txt`
- 本地映射缓存:`/sd/face_mapping.txt`
- 本地同步版本:`/sd/sync_version.txt`
- 本地人脸照片缓存:`/sd/face_photos/<face_id>.jpg`
## 4. 服务端交互接口 | 文件 | 作用 |
K210 在脚本中使用 HTTP 访问以下接口: | --- | --- |
- `GET /api/v1/sync/state?client=k210&last_version=<本地版本>`:按版本拉取统一同步状态。 | `k210/main.py` | K210 主程序,包含识别、添加、删除、同步、上传照片 |
- `POST /api/v1/sync/ack`:确认已完成删除任务。 | `k210/view_attendance.py` | 辅助脚本,用于查看旧版本地记录文件 |
- `GET /api/v1/sync/request`:轮询是否触发同步。
- `POST /api/v1/sync/request/ack`:确认收到同步请求。
- `GET /api/v1/faces/photos`:获取服务端已有照片列表,用于判断本地照片是否需要补传。
- `POST /api/v1/faces/photos/<face_id>`:上传 JPEG 人脸照片。
v1 兼容辅助接口: K210 程序依赖 SD 卡上的模型文件:
- `GET /api/v1/faces/mappings`
- `GET /api/v1/faces/pending-delete`
- `POST /api/v1/faces/delete-ack`
## 5. 启动与运行流程 ```text
1. 初始化摄像头、KPU 模型、LCD、按键中断、串口。 /sd/FaceDetection.smodel
2. 启动 ESP8266,尝试连接 WiFi。 /sd/FaceLandmarkDetection.smodel
3. 启动同步: /sd/FeatureExtraction.smodel
- 读取 `/sd/sync_version.txt`。 ```
- 请求统一同步状态。
- 先删除 `pending_delete` 中的人脸特征。
- 再应用 `face_mappings`。
- 成功后保存新的同步版本。
4. 进入主循环:识别 + 绘制 + 向 STM32 上报。
5. 分时网络任务(避免阻塞识别帧率):
- 新增人脸时先把 JPEG 保存到 `/sd/face_photos/`,再加入上传队列。
- 启动时先获取服务端照片列表,再扫描 `/sd/face_photos/`,只补传服务端缺失的照片;如果服务端不可达,则退回全量加入补传队列。
- 优先处理照片上传队列,上传失败会保留本地照片并等待下次重试。
- 然后处理同步任务。
## 6. 按键功能 如果模型文件不存在或路径错误,程序会在加载 KPU 模型时报错。
在 `k210/main.py` 中定义:
- 短按:添加人脸。
- 长按(约2秒):进入删除单个人脸模式。
- 超长按(约5秒):删除全部人脸。
## 7. 当前默认配置(代码内) ## 3. 代码中的主要配置
- WiFi:`YOUR_WIFI_SSID / YOUR_WIFI_PASSWORD`
- 服务端:`127.0.0.1:8082`(部署时改为实际服务端地址)
- API Token:`change-me-token`(部署时必须修改)
文件位置:`k210/main.py` 位置:`k210/main.py`
## 8. 部署注意事项 | 配置 | 当前值 | 说明 |
- SD 卡需包含模型文件: | --- | --- | --- |
- `/sd/FaceDetection.smodel` | `WIFI_SSID` | `yzfh` | K210 侧 ESP8266 要连接的 WiFi |
- `/sd/FaceLandmarkDetection.smodel` | `WIFI_PASSWORD` | `yzfhroot` | WiFi 密码 |
- `/sd/FeatureExtraction.smodel` | `SERVER_IP` | `27.106.113.205` | 服务端 IP |
- 与 STM32 使用统一学号语义(映射同步后应发送真实学号语义)。 | `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/<face_id>` | 上传 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 管理端设置映射。
+619 -114
View File
@@ -1,152 +1,657 @@
# 服务端文档 # 服务端文档
## 1. 推荐服务端版本 ## 1. 服务端的作用
当前项目推荐使用:`server/server_encrypted.cpp`
原因: 服务端是整个系统的数据中心。STM32、K210 和 Qt 管理端都通过 HTTP 与它通信。
- 支持 STM32 上传加密签到数据。
- 支持 Qt 管理端所需管理 API。
- 支持 K210 人脸照片与映射同步相关 API。
## 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 加密上传的签到记录。
1. STM32 调用 `POST /api/v1/attendance/checkins` 上传 Base64 密文。 2. 解密、校验并保存记录。
2. 服务端按 AES-128-CBC 解密并解析 JSON。 3. 提供考勤记录和统计数据给 Qt。
3. 将记录写入 `attendance.db` 的 `records` 表(含 `class_id` 字段)。 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`
说明: ```text
- `/api/v1/attendance/records` 与 `/api/v1/attendance/statistics` 默认返回明文 JSON;请求头包含 `X-Encrypted-Response: true` 时返回加密数据。 server/server_encrypted.cpp
```
### 4.2 时间段与查重 ## 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=`
### 4.3 映射与照片 | 项 | 内容 |
- `POST /api/v1/config/mappings` | --- | --- |
- `GET /api/v1/config/mappings` | 语言 | C++ |
- `GET /api/v1/faces/photos` | HTTP 实现 | 原生 socket + 每连接一个线程 |
- `GET /api/v1/faces/photos/<id>` | 数据库 | SQLite |
- `POST /api/v1/faces/photos/<face_id>` | 加密库 | OpenSSL EVP AES-128-CBC |
- `POST /api/v1/faces/mappings` | 图片存储 | 本地 `faces/*.jpg` |
- `GET /api/v1/faces/mappings` | 默认端口 | `8082` |
- `GET /api/v1/faces/student-ids` | 默认数据库 | `attendance.db` |
### 4.4 统一同步接口(推荐) ## 3. 运行时文件
- `GET /api/v1/sync/state?client=<client>&last_version=<version>`
- `POST /api/v1/sync/ack`
- `GET /api/v1/sync/version`
说明: 服务端启动后主要依赖当前工作目录下的文件。在哪个目录运行,就在哪个目录读写这些文件。
- 服务端是唯一权威状态源。
- 每次签到、清空/删除记录、修改时间段、修改映射、上传照片、删除人脸,服务端都会递增 `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`。 | `attendance.db` | 主 SQLite 数据库 |
- `client=qt`:返回时间段、人脸映射、照片列表、学生ID列表、记录、统计和映射配置。 | `faces/` | 人脸照片目录 |
- `client=all`:返回全部可同步状态。 | `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>
```
允许不带 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/<id>` | `/api/face_photo/<id>` 或 `/api/face_photo_simple/<id>` | 下载/上传照片 |
| `/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/<id>` | `/api/face/<id>` | 删除人脸 |
| `/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: <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=<client>&last_version=<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 ```json
{ {
"client": "k210", "client": "k210",
"version": 12, "version": 7,
"deleted": ["2", "3"], "deleted": ["1"],
"task_ids": ["2-1770000000", "3-1770000001"] "task_ids": ["task-1"]
} }
``` ```
### 4.5 人脸删除与同步控制 用途:
- `DELETE /api/v1/faces/<student_id>?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 旧接口兼容 ## 10. 编译与运行
服务端内部仍保留旧 `/api/...` 路由兼容层,便于历史程序临时访问;新代码和新文档统一使用 `/api/v1/...`。
## 5. 编译与运行 ### 10.1 Windows MinGW 示例
Windows(示例):
```bash 如果已经装好 MinGW、OpenSSL、SQLite:
cd server
```powershell
g++ -o server_encrypted.exe server_encrypted.cpp -lws2_32 -lcrypto -lssl -lsqlite3 -std=c++11 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 ```bash
cd server g++ -o server_encrypted server_encrypted.cpp -lcrypto -lssl -lsqlite3 -std=c++11 -pthread
g++ -o server_encrypted server_encrypted.cpp -lcrypto -lssl -lsqlite3 -lpthread -std=c++11
./server_encrypted ./server_encrypted
``` ```
说明:服务端启动时会自动创建 `attendance.db`。如果检测到旧 CSV/JSON 文件,会在首次启动时导入;之后以后端 API 和 SQLite 为准。 ### 10.3 运行目录建议
## 6. 备份与恢复 建议在 `server/` 目录下运行,这样数据库、照片目录和备份目录都会生成在 `server/` 下面:
服务端启动时如果发现已有 `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. 自动化回归
服务端启动后可运行:
```powershell ```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. 版本说明 1. 默认 Token 已写在代码里,不适合直接暴露公网。
仓库仅保留当前推荐版本:`server_encrypted.cpp`。旧版实验服务端与测试脚本已清理,避免误部署非鉴权接口。 2. 当前是 HTTP 明文传输,公网部署建议放到 HTTPS 反向代理后面。
3. AES Key/IV 是固定值,演示可以,生产环境不建议固定写死。
4. SQLite 数据库和照片目录需要定期备份。
5. `ATTENDANCE_CORS_ORIGIN` 不建议设置成任意来源。
6. 服务器运行目录要限制写权限,避免误删数据。
## 10. 关键一致性要求 ## 12. 常见问题
- AES Key/IV 必须与 STM32、Qt 完全一致。
- 若 Qt 无法解析记录,先检查是否误连到非加密版本服务端。
## 11. 后续优化
服务端当前已经完成 SQLite 主存储、v1 接口、自动备份和统一同步模型。后续需要继续跟踪的风险和优化项已统一记录在 `docs/05_项目审查问题清单.md`。
| 现象 | 可能原因 | 处理 |
| --- | --- | --- |
| 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/` 权限 |
+297 -489
View File
@@ -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/` | 桌面端图标 |
- 从服务端拉取考勤记录、统计信息和配置数据。 ## 3. 技术栈
- 查看、筛选、导出考勤记录。
- 按学生维度生成本地统计报表。
- 管理服务端地址、签到时间段、学生姓名映射、班级名称映射。
- 管理 K210 上传的人脸照片和人脸 ID 到学号的绑定关系。
- 在服务端不可达时给出简短状态提示,并自动重连。
当前 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` |
| 模块 | 路径 | 说明 | ## 4. 应用基础信息
| --- | --- | --- |
| 程序入口 | `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 图标与平台模板 |
## 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 ## 5. 页面结构
- Qt 6.2+
- Qt Quick / Qt Quick Controls 2
- Qt Core、Gui、Qml、Quick、QuickControls2、Network
- CMake 3.16+
界面由 `ApplicationWindow` 承载,内部使用 `StackLayout` 切换五个页面: Qt 管理端是单窗口应用,共 5 个页面:
- 考勤记录 | 页面 | 作用 |
- 统计分析 | --- | --- |
- 配置中心 | 考勤记录 | 查看、筛选、导出、清空记录 |
- 人脸管理 | 统计分析 | 按时间范围统计学生签到情况 |
- 关于 | 配置中心 | 服务端地址、签到时间段、名称映射 |
| 人脸管理 | 查看照片、绑定学号、删除人脸 |
| 关于 | 系统说明、版本和作者信息 |
QML 不直接访问网络。所有页面操作统一调用 `AppController` 暴露的 `Q_INVOKABLE` 方法和 `Q_PROPERTY` 状态,数据表格通过 `RecordListModel`、`StatisticsListModel`、`TimeSlotListModel`、`MappingListModel`、`FaceListModel` 渲染。 PC 宽屏显示左侧导航栏;窄屏和 Android 显示顶部/底部更紧凑的导航布局。
## 4. 当前已实现功能 ## 6. 网络访问规则
### 4.1 启动与服务连接 Qt 端的服务端地址默认是:
应用启动流程:
1. `main.cpp` 创建 `QGuiApplication`。
2. 设置应用名称、版本、组织名和窗口图标。
3. Android 平台启用原生文字渲染,改善中文粗体显示。
4. 创建 `AppController`。
5. 将 `appController` 注入 QML 上下文。
6. 加载 `qrc:/qml/Main.qml`。
`AppController` 构造时会:
- 读取本地服务端地址、映射配置和时间段配置。
- 设置 `ApiClient` 的服务端地址。
- 启动健康检查。
- 如果服务端可用,同步记录、统计、时间段和映射配置。
- 如果服务端不可用,进入自动重连流程。
默认服务端地址:
```text ```text
http://127.0.0.1:8082 http://127.0.0.1:8082
``` ```
用户可以在“配置中心”修改服务端地址。保存后会写入 `QSettings` 的 `server/url`,下次启动会读取并使用。 用户可在“配置中心”修改服务端地址。保存后写入本地 `QSettings`:
### 4.2 自动重连与状态提示 ```text
server/url
```
当前实现了基于 `/api/v1/system/health` 的连接检查: `ApiClient::buildUrl` 会在地址后自动拼接 `/api`,例如:
- 网络请求超时时间为 5 秒。 ```text
- 服务端不可达时显示简短提示:`服务器未连接,稍后重试`。 服务端地址: http://127.0.0.1:8082
- 自动重连间隔为 5 秒。 传入 endpoint: /v1/system/health
- 重连期间显示:`正在重连服务器`。 实际请求: http://127.0.0.1:8082/api/v1/system/health
- 服务恢复后自动刷新记录、统计、时间段、映射和人脸相关数据。 ```
底部 Toast 会展示状态信息,避免把底层网络错误、URL 和长异常文本直接暴露给用户。 所有请求都会带:
### 4.3 考勤记录查看 ```text
X-Api-Token: <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 ```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 ```text
考勤统计报表,<范围>
学号,姓名,总签到次数,今日签到,最后签到时间,平均置信度 学号,姓名,总签到次数,今日签到,最后签到时间,平均置信度
``` ```
统计结果不是服务端全量统计,而是对当前客户端已经拉取的最多 `1000` 条记录进行本地计算。 ## 13. 配置中心
### 4.7 配置中心 配置中心包含三块:
“配置中心”集中管理服务连接、签到时间段和名称映射。 1. 服务端地址。
2. 时间段配置。
3. 名称映射配置。
#### 服务端地址 ### 13.1 服务端地址
支持: 字段示例:
- 查看当前服务端地址。
- 修改并保存服务端地址。
- 测试服务端连接。
保存后会写入本地 `QSettings`:
```text ```text
server/url http://27.106.113.205:8082
``` ```
#### 签到时间段 按钮:
支持: | 按钮 | 作用 |
| --- | --- |
| 保存地址 | 写入 `QSettings`,并立即切换 `ApiClient` 的服务端 |
| 测试连接 | 请求 `/api/v1/system/health` |
- 查看当前时间段。 ### 13.2 时间段配置
- 新增时间段。
- 编辑时间段。
- 删除时间段。
- 恢复默认时间段。
- 从服务端同步时间段。
- 保存时间段到本地并上传服务端。
默认时间段: 时间段格式:
| 名称 | 开始时间 | 结束时间 | 默认状态 | ```json
| --- | --- | --- | --- | {
| 上午签到 | 08:00 | 09:30 | 启用 | "name": "上午签到",
| 中午签退 | 12:00 | 13:00 | 启用 | "start_time": "08:00",
| 下午签到 | 14:00 | 15:00 | 启用 | "end_time": "09:30",
| 晚上签退 | 17:00 | 18:30 | 启用 | "enabled": true
}
```
当前默认时间段为空。第一次使用时需要手动新增。
按钮:
| 按钮 | 作用 |
| --- | --- |
| 展开/收起 | 展示或隐藏时间段列表 |
| 新增时段 | 打开新增对话框 |
| 清空时段 | 清空本地时间段并保存同步到服务端 |
| 从服务器同步/同步 | 从服务端读取时间段;服务端为空数组时也会清空本地时间段 |
| 保存时间段/保存 | 保存到本地并上传服务端 |
本地文件: 本地文件:
@@ -230,20 +289,25 @@ server/url
timeslot_config.json timeslot_config.json
``` ```
#### 名称映射 如果本地文件中刚好是旧版本四个默认时间段,当前代码会自动迁移为空配置。
支持两类映射: ### 13.3 名称映射配置
- 学号 -> 姓名 学生映射:
- 班级编号 -> 班级名称
支持操作: ```json
{
"20240001": "张三"
}
```
- 新增映射。 班级映射:
- 直接编辑映射键和值。
- 删除映射。 ```json
- 从服务端加载映射配置。 {
- 保存到本地并上传服务端。 "1": "软件工程一班"
}
```
本地文件: 本地文件:
@@ -251,378 +315,122 @@ timeslot_config.json
mapping_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/<id>`。
3. 下载成功后保存到本地缓存目录:
```text ```text
ApiClient -> GET /api/v1/faces/photos/{id} -> 本地 file:// 缓存 -> QML Image <AppDataLocation>/face_photos/<id>.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 特殊处理:
| 方法 | 路径 | 用途 | 1. 最小窗口宽度是 `360`。
| --- | --- | --- | 2. 最小窗口高度是 `560`。
| `GET` | `/api/v1/attendance/records?limit={limit}&offset={offset}` | 获取考勤记录 | 3. 字体使用 `sans-serif`。
| `GET` | `/api/v1/attendance/statistics` | 获取总数、今日数和服务端时间 | 4. 粗体权重降低为 `Font.Medium`。
| `POST` | `/api/v1/attendance/records/clear` | 清空全部记录 | 5. 使用原生文字渲染,减少中文粗体重影。
| `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` | 健康检查 |
### 5.1 考勤记录格式 配置页卡片现在使用内容自适应高度,避免按钮遮挡。
`/api/v1/attendance/records` 解密后应返回 JSON 数组,单条记录示例: ## 17. 本地持久化
```json Qt 使用两类本地持久化。
{
"timestamp": "2026-04-24 08:10:23",
"student_id": "20240001",
"name": "张三",
"confidence": 0.9821,
"status": "success",
"class_id": "1"
}
```
字段处理规则: ### 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 ```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 | | `timeslot_config.json` | 时间段配置 |
| Padding | PKCS#7 | | `mapping_config.json` | 学生姓名和班级名称映射 |
| Key | `2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c` | | `face_photos/*.jpg` | 下载后的人脸照片缓存 |
| IV | `00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f` |
如果服务端没有按加密格式返回,记录和统计接口会解析失败。 ## 18. 构建方式
## 7. 本地配置持久化 ### 18.1 Qt Creator
应用使用 `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 构建
1. 打开 Qt Creator。 1. 打开 Qt Creator。
2. 打开 `QT/CMakeLists.txt`。 2. 打开 `QT/CMakeLists.txt`。
3. 选择 Desktop Qt 6.x 或 Android Kit。 3. 选择 Desktop Qt 6.x 或 Android Kit。
4. 等待 CMake 配置完成。 4. 等待 CMake 配置完成。
5. 构建目标 `apprewrite_qt`。 5. 构建 `apprewrite_qt`。
6. 桌面端直接运行;Android 端部署到设备或模拟器。 6. 桌面端直接运行;Android 端部署到设备或模拟器。
### 8.3 命令行构建 ### 18.2 命令行示例
Windows 示例: Windows:
```powershell ```powershell
cmake -S QT -B QT/build -DCMAKE_PREFIX_PATH=D:/Qt/6.10.0/mingw_64 cd D:\University\graduation\code\Reconstruct_Qt\QT
cmake --build QT/build --target apprewrite_qt cmake -S . -B build\Desktop -DCMAKE_PREFIX_PATH=D:\Qt\6.10.0\mingw_64
cmake --build build\Desktop
``` ```
Linux 示例: 实际路径要按本机 Qt 安装位置调整。
```bash ## 19. 常见问题
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 转义。后续如果要提升为更完整的管理端,应优先补充分页加载或服务端筛选统计接口,再补齐记录级管理操作。
| 现象 | 可能原因 | 处理 |
| --- | --- | --- |
| 测试连接失败 | 服务端没启动、地址错、Token 不一致 | 检查服务端健康接口和 `ATTENDANCE_API_TOKEN` |
| 页面按钮重叠 | 旧版固定高度导致内容被裁切 | 当前已改为自适应高度 |
| Android 文字发虚/重影 | 字体渲染方式问题 | 当前 Android 使用原生渲染和中等粗体 |
| 人脸照片不显示 | 下载失败、本地缓存路径无效、服务端无照片 | 刷新人脸库,看服务端 `faces/` |
| 统计数量少于服务端总量 | Qt 默认只拉取 1000 条记录 | 增加分页或服务端统计接口 |
| 时间段为空导致无法签到 | 当前默认就是空配置 | 在配置中心新增时间段并保存 |
| 保存映射后 K210 没变化 | K210 尚未同步或 WiFi 不通 | 等待同步或检查 K210 日志 |
+229 -290
View File
@@ -1,344 +1,283 @@
# 项目审查问题清单 # 项目维护、验收与风险清单
审查日期:2026-05-01 ## 1. 文档目的
本文档用于记录当前项目中已发现的高风险问题,并作为后续逐项修复的跟踪清单。状态说明: 这份文档用于回答三个问题:
- `待处理`:尚未开始修复。 1. 项目目前实现了什么。
- `处理中`:已经开始改动或验证。 2. 验收或答辩时可以从哪些方面说明。
- `已修复`:代码已修复并通过基本验证。 3. 后续维护时有哪些风险和改进方向。
- `暂缓`:因演示环境、硬件依赖或其他原因暂不处理。
## 1. 优先级概览 它不是单纯的 bug 列表,而是把功能完整性、稳定性、安全性、易用性和可维护性都列出来。
| 编号 | 优先级 | 状态 | 模块 | 问题 | ## 2. 当前实现状态总览
| --- | --- | --- | --- | --- |
| 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. 详细问题 | 模块 | 当前状态 | 说明 |
| --- | --- | --- |
| STM32 主控 | 已实现 | 接收 K210、考勤判断、加密上传、断线重试、时间段同步 |
| K210 识别 | 已实现 | 人脸检测、特征比对、添加/删除、照片上传、映射同步 |
| 服务端 | 已实现 | SQLite 存储、HTTP API、Token、AES 解密、统一同步 |
| Qt 管理端 | 已实现 | 记录、统计、配置、人脸管理、PC/Android 响应式界面 |
| 默认时间段 | 已改为空 | 需要管理员在 Qt 中新增 |
| 数据导出 | 已实现 | Qt 可导出记录和统计 CSV |
| 自动化 API 回归 | 已有脚本 | `server/regression_api_tests.ps1` |
### R-001 公网接口无鉴权且允许任意来源 ## 3. 功能验收清单
- 优先级:P0 ### 3.1 服务端验收
- 状态:已修复
- 位置:`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`。
### 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 ### 3.2 Qt 管理端验收
- 状态:部分修复
- 位置:`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。
### R-003 `face_id` 可造成路径穿越写读删文件 | 检查项 | 通过标准 |
| --- | --- |
| 启动界面 | Windows 和 Android 都能打开主界面 |
| 服务地址 | 可保存地址并测试连接 |
| 配置页布局 | PC 和 Android 不出现按钮遮挡 |
| 时间段 | 默认为空,可新增、保存、同步、清空 |
| 映射 | 可新增学生姓名和班级名称映射 |
| 记录页 | 能加载、筛选和导出记录 |
| 统计页 | 能按日期范围统计并导出 |
| 人脸管理 | 能刷新照片、绑定学号、保存映射、删除人脸 |
| 自动重连 | 服务端恢复后能重新同步 |
- 优先级:P1 ### 3.3 STM32 验收
- 状态:已修复
- 位置:`server/server_encrypted.cpp:837`
- 影响:`face_id` 直接来自 URL 并拼接成 `faces/<face_id>.jpg`,用于写入、读取和删除。包含路径分隔符或 `..` 时,可能越过 `faces` 目录操作其他文件。
- 建议处理:
1. 增加 `face_id` 白名单校验,只允许数字、字母、下划线、短横线等明确字符。
2. 对最终路径做规范化检查,确认仍位于 `faces` 目录下。
3. 所有照片读写删入口共用同一个安全路径构造函数。
- 本次处理:主服务端已增加 `isSafeId` 与 `buildFacePhotoPath`,照片读写删和重命名路径统一走安全 ID 校验。
### R-004 大请求体会被静默截断 | 检查项 | 通过标准 |
| --- | --- |
| 硬件初始化 | OLED、蜂鸣器、LED、串口正常 |
| ESP8266 | 能连接 WiFi 和服务端 |
| 时间同步 | 能从服务端获取时间或读取 DS1302 |
| 时间段同步 | 能拉取服务端配置,空配置会清空本地时间段 |
| K210 接收 | 能解析 `ID:...,NAME:...,CONF:...` |
| 签到判断 | 置信度、时间段、重复签到逻辑生效 |
| 加密上传 | 服务端能收到并解密保存记录 |
| 断线重试 | 网络断开时记录保留,恢复后同步 |
- 优先级:P1 ### 3.4 K210 验收
- 状态:已修复
- 位置:`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。
### R-005 CSV/JSON 没有转义会破坏数据和响应 | 检查项 | 通过标准 |
| --- | --- |
| 模型加载 | 三个 `.smodel` 正常加载 |
| 摄像头 | LCD 能显示画面,方向正确 |
| 人脸识别 | 已录入人脸能识别,陌生人显示陌生 |
| 添加人脸 | 短按进入添加,确认后写入 SD 卡 |
| 删除人脸 | 长按进入删除,可删除单个或全部 |
| 照片上传 | 新增人脸照片能上传到服务端 |
| 映射同步 | Qt 保存映射后 K210 本地名称更新 |
| 删除同步 | Qt 删除后 K210 能删除本地特征并 ACK |
- 优先级:P1 ## 4. 数据一致性清单
- 状态:已修复
- 位置:`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 仅作为首次导入来源。
### 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 ### 4.2 服务端地址一致
- 状态:已修复
- 位置:`server/server_encrypted.cpp:1310`
- 影响:`limit`、`offset`、时间段参数直接调用 `std::stoi`,没有异常捕获和范围校验。非法请求参数可能让请求处理线程异常终止。
- 建议处理:
1. 增加安全整数解析函数。
2. 参数非法时返回 400。
3. 限制 `limit`、`offset` 和时间范围。
4. 所有 URL 参数统一走校验函数。
- 本次处理:主服务端已增加安全整数和时间解析,`limit`、`offset`、时间段和记录时间解析不再直接裸用 `std::stoi`。
### R-008 删除人脸 URL 未编码 这些地方要指向同一个服务端:
- 优先级:P2 1. Qt 配置中心的服务端地址。
- 状态:已修复 2. `User/main.c` 的 `HTTP_SERVER_IP` 和 `HTTP_SERVER_PORT`。
- 位置:`QT/src/ApiClient.cpp:453` 3. `k210/main.py` 的 `SERVER_IP` 和 `SERVER_PORT`。
- 影响:Qt 端删除人脸时直接把 `studentId` 拼进路径。如果学号包含空格、`#`、`?`、`/` 或中文,URL 会被截断或路由错乱。
- 建议处理:
1. 使用 `QUrl::toPercentEncoding` 编码路径段。
2. 服务端接收后进行 URL 解码和 ID 校验。
3. 为删除人脸接口补充异常 ID 的测试用例。
- 本次处理:Qt 删除人脸路径段已使用 `QUrl::toPercentEncoding` 编码。
### 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 ### 4.4 时间段一致
- 状态:已修复
- 位置:`Hardware/Attendance.c:75-108`
- 影响:当前缓存只保存 `student_id`,不保存考勤时段。任一时段签到成功后都会进入缓存,后续中午、下午、晚上时段再签到可能被 `Attendance_IsDuplicateInCache` 判为重复。开机不在时段时还会按 `00:00-23:59` 拉取全天记录,误判范围更大。
- 建议处理:
1. 缓存项增加时段编号或 `slot_start/slot_end` 字段。
2. 本地新增签到写缓存时同时写入当前时段。
3. 服务器下载今日记录时只填充当前有效时段的缓存;不在时段内不要拉取全天缓存用于查重。
4. 切换时段时清理或重建缓存,避免跨时段污染。
- 本次处理:`Hardware/Attendance.c` 的缓存项增加时段绑定;新增签到和服务器下载缓存都写入当前时段;不在时段内不再拉取全天缓存;时段切换时自动清空缓存。
### R-012 签到请求手写 JSON 解析会被特殊字符破坏 Qt 保存时间段后:
- 优先级:P1 1. 服务端 `time_slots` 更新。
- 状态:已修复 2. 服务端同步版本提升。
- 位置:`server/server_encrypted.cpp:707-742` 3. STM32 下一次同步拉取到新时间段。
- 影响:`parseJSON` 直接按逗号拆字段、按第一个冒号拆键值,无法正确处理字符串里的逗号、冒号、反斜杠和转义引号。签到姓名、状态或后续字段一旦包含这些字符,会解析错列甚至写入错误数据。 4. STM32 使用新的时间段判断签到。
- 建议处理:
1. 引入 `nlohmann/json` 等成熟 JSON 库,优先替换签到请求体解析。
2. 对必填字段做类型、长度和取值校验。
3. JSON 解析失败时返回 400,避免写入半解析数据。
4. 增加包含逗号、冒号、引号和反斜杠的解析测试样例。
- 本次处理:`server/server_encrypted.cpp` 的签到 JSON 解析改为逐字符解析字符串字面量,支持逗号、冒号、反斜杠和转义引号;签到字段增加 ID、长度、置信度范围和状态取值校验,非法请求返回 400。
### R-013 K210 从 SD 卡加载特征时使用 `eval` ### 4.5 人脸映射一致
- 优先级:P2 Qt 保存人脸映射后:
- 状态:已修复
- 位置:`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`,只接受浮点数组允许字符,并校验向量长度,异常记录会被跳过并打印解析错误。
### R-014 总文档指向不存在的 Qt 工程文件 1. 服务端保存 `face_mappings`。
2. 服务端触发 K210 同步请求。
3. K210 同步后更新 `face_mapping.txt`。
4. K210 更新 `recordftr.txt`。
5. K210 发给 STM32 的 ID 变成真实学号。
- 优先级:P2 ## 5. 当前已知限制
- 状态:已修复
- 位置:`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` 前同步清理了相关引用。
### R-015 数据库缺少自动备份与恢复流程 ### 5.1 Qt 记录统计基于当前加载记录
- 优先级:P2 Qt 默认请求:
- 状态:已修复
- 位置:`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` 已补恢复和校验流程。
### R-016 删除任务缺少 task_id 和状态跟踪 ```text
limit=1000&offset=0
```
- 优先级:P2 如果服务端已有超过 1000 条记录,Qt 本地筛选和统计只覆盖当前加载的这 1000 条。服务端 `/statistics` 的总数和今日数仍来自数据库。
- 状态:已修复
- 位置:`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` 确认。
### R-017 设备同步状态不可追踪 改进方向:
- 优先级:P2 1. 增加分页。
- 状态:已修复 2. 增加服务端按条件查询。
- 位置:`server/server_encrypted.cpp` 3. 增加服务端按日期统计接口。
- 影响:各端只在本地保存自己的 `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` 摘要。
### R-018 公网 HTTP 明文传输缺少 HTTPS 或反向代理保护 ### 5.2 时间输入缺少强校验
- 优先级:P2 Qt 时间段输入框允许手动输入文本。当前主要依赖 `QTime::fromString("HH:mm")` 解析。
- 状态:已修复
- 位置:`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 后再对外暴露。
### R-019 关键接口缺少自动化回归脚本 改进方向:
- 优先级:P3 1. 使用时间选择控件。
- 状态:已修复 2. 保存前校验格式。
- 位置:`docs/06_后端接口Windows测试命令.md`、待新增测试脚本 3. 检查开始时间必须早于结束时间。
- 影响:当前主要依赖手动 curl 文档验证接口。服务端、Qt、STM32、K210 协议经常一起变动,缺少脚本化回归会让鉴权、同步状态、时间段配置、照片接口和记录接口的破坏更晚被发现。 4. 检查时间段重叠。
- 建议处理:
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 等低风险接口;手动命令文档已加入脚本使用方式。
### R-020 删除 ACK 失败后本地版本仍前进,可能卡住删除任务 ### 5.3 K210 JSON 解析仍偏轻量
- 优先级:P1 K210 MicroPython 环境中很多 JSON 解析是手写字符串处理。简单数据够用,但复杂转义字符支持有限。
- 状态:已修复
- 位置:`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 失败时将本轮同步标记为失败,不保存新的本地同步版本,等待下一轮继续重试。
### R-021 多设备或缓存过期时重复签到仍可能漏判 改进方向:
- 优先级:P2 1. 尽量让服务端返回简单结构。
- 状态:已修复 2. 避免映射 key/value 中出现引号、逗号等复杂字符。
- 位置:`Hardware/Attendance.c:520-529` 3. 如果固件环境支持,使用 JSON 库。
- 影响:当前签到处理主要检查 STM32 本地记录和开机/重连时下载的今日缓存。`Attendance_IsDuplicateOnServer` 已存在,但没有接入主签到路径。如果现场只有一台设备演示,影响较小;如果多台设备同时使用,或设备启动后缓存长时间未刷新,服务端已有的同一时段签到记录可能不会被当前设备发现,导致重复签到。
- 建议处理:
1. 在 `g_server_connected` 为真时,签到前调用一次 `Attendance_IsDuplicateOnServer`。
2. 查询失败时仍允许本地签到并进入待同步队列,避免弱网影响正常演示。
3. 时段切换、服务器重连成功后刷新今日记录缓存。
4. 如果只保留单设备演示,也应在论文或文档中说明本地缓存查重的适用范围。
- 备注:毕业设计阶段不需要做复杂分布式一致性,优先保证演示环境下行为可解释。
- 本次处理:`Hardware/Attendance.c` 在本地记录和缓存查重之后,服务器在线时调用 `Attendance_IsDuplicateOnServer` 做一次服务端查重;查询失败仍按未重复处理,避免弱网阻断演示。`User/main.c` 在 WiFi/HTTP 服务端重连成功后刷新时间段配置和今日记录缓存。
### R-022 HTTP GET 可能接受半截响应 ### 5.4 STM32 本地缓存容量有限
- 优先级:P2 STM32 本地记录最多 100 条,今日学生缓存最多 50 个 ID。
- 状态:已修复
- 位置:`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`,必须等到响应体长度达到声明长度才返回成功。
## 3. 建议修复顺序 改进方向:
1. 已完成 R-020:K210 删除 ACK 失败时不再推进本地同步版本,避免服务端待删除任务卡住。 1. 根据实际班级人数扩大缓存。
2. 已完成 R-022:ESP8266 GET 在有 `Content-Length` 时必须收完整响应,减少弱网下配置同步解析失败。 2. 增加外部 Flash 或 SD 卡存储。
3. 已完成 R-021:STM32 在线签到前补服务端查重,重连后刷新今日记录缓存。 3. 对已同步记录做更明确的淘汰策略。
4. R-002 仍需真实环境人工轮换已暴露凭据;代码层面已尽量外部配置化和脱敏。
5. 已修复项保持回归关注:R-001、R-003、R-004、R-005、R-006、R-007、R-008、R-009 至 R-022。
## 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 编译和上板验证。
+485 -279
View File
@@ -1,340 +1,546 @@
# 后端接口 Windows 手动测试命令 # 后端接口 Windows 测试命令
当前后端 `server_encrypted.cpp` 只监听 HTTP 端口: ## 1. 使用前准备
```text 本文档用于在 Windows PowerShell 中手动测试服务端接口。
8082
```
以下命令适合在 Windows PowerShell 或 CMD 中复制执行。Windows 下建议使用 `curl.exe`,避免 PowerShell 把 `curl` 解析成别名。 默认服务端地址:
如果后端在本机运行,使用:
```text
http://127.0.0.1:8082
```
如果测试公网服务器,替换成:
```text
https://your-domain.example
```
下面示例默认使用本机地址。
## 0. 自动化回归脚本
服务端启动后,优先运行脚本化检查:
```powershell ```powershell
cd server $BaseUrl = "http://127.0.0.1:8082"
powershell -ExecutionPolicy Bypass -File .\regression_api_tests.ps1 -BaseUrl "http://127.0.0.1:8082" -ApiToken "change-me-token"
``` ```
该脚本覆盖健康检查、存储状态、同步状态、记录、统计、时间段、映射和照片列表等低风险接口。需要真实硬件、真实 AES 正文或真实 JPEG 文件的接口仍按下方手动命令验证。 默认 Token:
## 1. 测试端口和服务健康
```powershell ```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` 或类似成功响应。 建议先在 PowerShell 中设置:
## 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 ```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 ```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 ```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 ### 3.2 带 Token 健康检查
"version": 当前服务端同步版本
"changed": true/false
"face_mappings": 人脸ID到学号映射
"pending_delete": K210需要删除的本地人脸ID
```
## 2.2 统一同步状态:STM32
```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/sync/state?client=stm32&last_version=0" Invoke-RestMethod -Uri "$BaseUrl/api/v1/system/health" -Headers $Headers
``` ```
重点看 `time_slots` 是否正确返回。 ### 3.3 端口连通
## 2.3 统一同步状态:Qt
```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/sync/state?client=qt&last_version=0" Test-NetConnection -ComputerName 127.0.0.1 -Port 8082
``` ```
## 2.4 K210 确认统一同步删除 公网服务器:
把 `2` 换成真实已删除成功的人脸 ID。
```powershell ```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. 获取全部考勤记录 ### 3.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/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 上检查端口占用
```powershell ```powershell
netstat -ano | findstr :8082 netstat -ano | findstr :8082
``` ```
如果能看到 `LISTENING`,说明本机有程序正在监听 `8082`。 ## 4. 系统接口
## 28. Windows 上测试 TCP 端口连通 ### 4.1 获取服务器时间
```powershell ```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 ```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 ```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: <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 写错。
+18
View File
@@ -69,6 +69,8 @@ const size_t MAX_PHOTO_BODY_SIZE = 512 * 1024;
const char* DEFAULT_API_TOKEN = "atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV"; const char* DEFAULT_API_TOKEN = "atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV";
const char* DEFAULT_CORS_ORIGIN = "http://localhost"; const char* DEFAULT_CORS_ORIGIN = "http://localhost";
const char* DEFAULT_TIMESLOTS_JSON = const char* DEFAULT_TIMESLOTS_JSON =
"{\"time_slots\":[]}";
const char* LEGACY_DEFAULT_TIMESLOTS_JSON =
"{\"time_slots\":[" "{\"time_slots\":["
"{\"name\":\"上午签到\",\"start_time\":\"08:00\",\"end_time\":\"09:30\",\"enabled\":true}," "{\"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\":\"12:00\",\"end_time\":\"13:00\",\"enabled\":true},"
@@ -139,6 +141,17 @@ std::string jsonEscape(const std::string& value) {
return out.str(); 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<unsigned char>(ch))) {
compact.push_back(ch);
}
}
return compact;
}
std::string csvEscape(const std::string& value) { std::string csvEscape(const std::string& value) {
bool needs_quotes = false; bool needs_quotes = false;
for (char c : value) { for (char c : value) {
@@ -1300,6 +1313,11 @@ bool saveTimeSlots(const std::string& json_data) {
std::string readTimeSlots() { std::string readTimeSlots() {
std::string content = sqliteGetKvValue("time_slots", DEFAULT_TIMESLOTS_JSON); 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; std::cout << "[TIMESLOTS] Read " << content.size() << " bytes from SQLite" << std::endl;
return content; return content;
} }