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