Initial QT project upload

This commit is contained in:
2026-05-04 02:48:33 +08:00
commit fca1ea21d2
43 changed files with 7367 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
# Build directories
build/
build-*/
cmake-build-*/
out/
dist/
# CMake generated files
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
DartConfiguration.tcl
# Qt Creator local settings
.qtcreator/
qtcreator-cache-backup-*/
*.user
*.user.*
# Qt generated artifacts
*_autogen/
*.qmlc
*.jsc
qrc_*.cpp
moc_*.cpp
moc_*.h
ui_*.h
# Compiled binaries and libraries
*.exe
*.dll
*.so
*.dylib
*.a
*.lib
*.o
*.obj
*.pdb
*.ilk
# Android build outputs
android-build/
*.apk
*.aab
# Logs and temporary files
*.log
*.tmp
*.temp
*.bak
*.swp
*~
# Local config and runtime data
.env
.env.*
*.db
*.db-shm
*.db-wal
faces/
face_photos/
backups/
logs/
# OS files
.DS_Store
Thumbs.db
desktop.ini
+146
View File
@@ -0,0 +1,146 @@
cmake_minimum_required(VERSION 3.16)
project(rewrite_qt VERSION 1.0.0 LANGUAGES CXX)
set(REWRITE_QT_DISPLAY_NAME "人脸识别考勤系统")
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
set(REWRITE_QT_OUTPUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dist")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${REWRITE_QT_OUTPUT_DIR}")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${REWRITE_QT_OUTPUT_DIR}")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${REWRITE_QT_OUTPUT_DIR}")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOMOC_COMPILER_PREDEFINES OFF)
set(CMAKE_AUTORCC ON)
file(TO_CMAKE_PATH "$ENV{LOCALAPPDATA}" REWRITE_QT_LOCALAPPDATA)
if(REWRITE_QT_LOCALAPPDATA)
set(REWRITE_QT_AUTOGEN_DIR "${REWRITE_QT_LOCALAPPDATA}/rewrite_qt_autogen")
else()
set(REWRITE_QT_AUTOGEN_DIR "${CMAKE_BINARY_DIR}/autogen_ascii")
endif()
file(MAKE_DIRECTORY "${REWRITE_QT_AUTOGEN_DIR}")
find_package(Qt6 6.2 REQUIRED COMPONENTS Core Gui Qml Quick QuickControls2 Network)
get_target_property(REWRITE_QT_QMAKE_EXECUTABLE Qt6::qmake IMPORTED_LOCATION)
if(REWRITE_QT_QMAKE_EXECUTABLE)
execute_process(
COMMAND "${REWRITE_QT_QMAKE_EXECUTABLE}" -query QT_INSTALL_QML
OUTPUT_VARIABLE REWRITE_QT_INSTALL_QML
OUTPUT_STRIP_TRAILING_WHITESPACE
)
endif()
qt_standard_project_setup()
set(REWRITE_QT_APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/favicon.ico")
set(REWRITE_QT_ICON_RESOURCES
assets/favicon.ico
assets/favicon-256x256.png
assets/favicon-192x192.png
assets/favicon-128x128.png
assets/favicon-64x64.png
assets/favicon-48x48.png
assets/favicon-32x32.png
assets/favicon-16x16.png
)
qt_add_executable(apprewrite_qt
main.cpp
src/AppController.cpp
src/ApiClient.cpp
src/CryptoUtils.cpp
src/RecordListModel.cpp
src/StatisticsListModel.cpp
src/TimeSlotListModel.cpp
src/StudentProfileListModel.cpp
src/MappingListModel.cpp
src/FaceListModel.cpp
)
if(REWRITE_QT_INSTALL_QML)
target_compile_definitions(apprewrite_qt PRIVATE
REWRITE_QT_INSTALL_QML="${REWRITE_QT_INSTALL_QML}"
)
endif()
if(WIN32 AND EXISTS "${REWRITE_QT_APP_ICON}")
set(REWRITE_QT_WIN32_ICON_RC "${CMAKE_CURRENT_BINARY_DIR}/app_icon.rc")
configure_file(cmake/app_icon.rc.in "${REWRITE_QT_WIN32_ICON_RC}" @ONLY)
target_sources(apprewrite_qt PRIVATE "${REWRITE_QT_WIN32_ICON_RC}")
endif()
if(ANDROID)
set_target_properties(apprewrite_qt PROPERTIES
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"
)
endif()
target_include_directories(apprewrite_qt PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
set(REWRITE_QT_RESOURCE_FILES
qml/Main.qml
)
foreach(REWRITE_QT_ICON_RESOURCE IN LISTS REWRITE_QT_ICON_RESOURCES)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${REWRITE_QT_ICON_RESOURCE}")
list(APPEND REWRITE_QT_RESOURCE_FILES "${REWRITE_QT_ICON_RESOURCE}")
endif()
endforeach()
qt_add_resources(apprewrite_qt "qml_resources"
PREFIX "/"
FILES
${REWRITE_QT_RESOURCE_FILES}
)
target_link_libraries(apprewrite_qt
PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Qml
Qt6::Quick
Qt6::QuickControls2
Qt6::Network
)
set_target_properties(apprewrite_qt PROPERTIES
AUTOGEN_BUILD_DIR "${REWRITE_QT_AUTOGEN_DIR}"
QT_QML_MODULE_NO_IMPORT_SCAN TRUE
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
)
include(GNUInstallDirs)
if(UNIX AND NOT APPLE AND NOT ANDROID)
set(REWRITE_QT_DESKTOP_FILE "${CMAKE_CURRENT_BINARY_DIR}/rewrite_qt.desktop")
configure_file(cmake/rewrite_qt.desktop.in "${REWRITE_QT_DESKTOP_FILE}" @ONLY)
install(FILES "${REWRITE_QT_DESKTOP_FILE}"
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications"
)
foreach(REWRITE_QT_ICON_SIZE IN ITEMS 16 32 48 64 128 192 256)
set(REWRITE_QT_LINUX_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/favicon-${REWRITE_QT_ICON_SIZE}x${REWRITE_QT_ICON_SIZE}.png")
if(EXISTS "${REWRITE_QT_LINUX_ICON}")
install(FILES "${REWRITE_QT_LINUX_ICON}"
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${REWRITE_QT_ICON_SIZE}x${REWRITE_QT_ICON_SIZE}/apps"
RENAME rewrite_qt.png
)
endif()
endforeach()
endif()
install(TARGETS apprewrite_qt
BUNDLE DESTINATION .
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Jitianye
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+180
View File
@@ -0,0 +1,180 @@
# Qt 管理端 README
这是人脸识别考勤系统的 Qt 管理端,支持 Windows 桌面端和 Android 端。更完整的说明见:
```text
../docs/04_Qt管理端文档.md
../docs/00_项目总文档.md
```
## 1. 功能概览
Qt 管理端用于连接考勤服务端,完成以下工作:
1. 查看考勤记录。
2. 按班级、学号、姓名和日期筛选记录。
3. 导出考勤记录 CSV。
4. 清空服务端考勤记录。
5. 查看学生维度的统计分析。
6. 导出统计 CSV。
7. 配置服务端地址和 API Token。
8. 新增、编辑、删除、清空签到时间段。
9. 配置学号到姓名、学号到班级编号、班级编号到班级名称的显示映射。
10. 查看 K210 上传的人脸照片。
11. 为 K210 face_id 绑定真实学号。
12. 删除人脸信息,并可选择删除关联考勤记录。
## 2. 当前重要行为
### 2.1 默认服务端地址
```text
http://127.0.0.1:8082
```
启动后会优先读取本地保存的 `server/url`,API Token 会优先读取 `server/apiToken`,没有本地值时再使用环境变量。
### 2.2 默认签到时间段
当前默认签到时间段为空:
```json
{
"time_slots": []
}
```
第一次使用时,需要在“配置中心”里新增时间段并保存同步到服务端;当前以服务端 SQLite 中的时间段配置为准。
### 2.3 API Token
Qt 每个请求都会带:
```text
X-Api-Token: <Token>
```
API Token 不写入源码。可以通过“配置中心”保存,也可以通过环境变量提供:
```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 |
| Qt 模块 | Core、Gui、Qml、Quick、QuickControls2、Network |
| 构建 | CMake 3.16+ |
| 平台 | Windows 桌面端、Android |
## 4. 目录结构
```text
QT/
├── android/ # Android 打包配置和图标
├── assets/ # 桌面端图标
├── cmake/ # 平台模板
├── qml/Main.qml # 主界面
├── src/
│ ├── ApiClient.* # HTTP API 客户端
│ ├── AppController.* # 业务逻辑中枢
│ ├── AttendanceTypes.h # 数据结构
│ ├── CryptoUtils.* # AES 解密工具
│ └── *ListModel.* # QML 列表模型
├── CMakeLists.txt
├── clear_qt_cache.ps1 # 清理 Qt Creator/CMake 本地缓存
├── LICENSE
├── README.md
└── main.cpp
```
## 5. 构建环境
基础要求:
1. Qt 6.2 或更高。
2. CMake 3.16 或更高。
3. 支持 C++17 的编译器。
Windows 推荐使用 Qt 6.x MinGW 64-bit Kit。
Android 还需要:
1. Qt for Android。
2. Android SDK。
3. Android NDK。
4. JDK。
5. Android 平台版本,例如 `android-35`。
## 6. Qt Creator 构建
1. 打开 Qt Creator。
2. 打开 `QT/CMakeLists.txt`。
3. 选择 Desktop Qt 或 Android Kit。
4. 等待 CMake 配置完成。
5. 构建目标 `apprewrite_qt`。
6. 桌面端直接运行;Android 端部署到设备或模拟器。
## 7. 命令行构建示例
Windows 示例:
```powershell
cd D:\University\graduation\thesis\code\QT
cmake -S . -B build\Desktop -DCMAKE_PREFIX_PATH=D:\Qt\6.10.0\mingw_64
cmake --build build\Desktop
```
Qt 安装路径需要按本机实际情况修改。
## 8. 迁移后清理缓存
项目移动到新目录后,Qt Creator 可能仍沿用旧的 `.qtcreator`、`build` 或 CMake 缓存。可在 Qt 工程目录运行:
```powershell
cd D:\University\graduation\thesis\code\QT
powershell -ExecutionPolicy Bypass -File .\clear_qt_cache.ps1
```
该脚本只清理当前项目目录内的 Qt/CMake 缓存,不清理 Qt Creator 全局配置,也不会备份。
## 9. 配置中心
服务端配置区包含“服务器地址”和“API Token”两个输入框:
| 按钮 | 作用 |
| --- | --- |
| 保存 | 同时保存服务器地址和 API Token 到 `QSettings`,并切换 `ApiClient` 配置 |
| 测试连接 | 使用当前配置请求 `/api/v1/system/health` |
## 10. 本地数据
Qt 会保存这些本地数据:
| 数据 | 保存方式 |
| --- | --- |
| 服务端地址 | `QSettings`,键名 `server/url` |
| API Token | `QSettings`,键名 `server/apiToken` |
| 时间段配置 | `QStandardPaths::AppDataLocation/timeslot_config.json` |
| 学生/班级映射 | `QStandardPaths::AppDataLocation/mapping_config.json` |
| 人脸照片缓存 | `QStandardPaths::AppDataLocation/face_photos/*.jpg` |
## 11. 常见问题
| 现象 | 处理 |
| --- | --- |
| 测试连接失败 | 检查服务端是否启动、地址是否正确、Token 是否一致 |
| 迁移目录后 Qt Creator 报错或构建目录不对 | 关闭 Qt Creator 后运行 `clear_qt_cache.ps1`,再重新打开 `CMakeLists.txt` |
| 没有时间段 | 这是当前默认行为,需要手动新增并保存 |
| 识别成功但没有记录 | 检查 STM32 是否在时间段内、是否成功加密上传 |
| 人脸照片不显示 | 检查 K210 是否上传照片,服务端 `faces/` 是否有图片 |
| Android 布局拥挤 | 当前 QML 已做窄屏自适应,若仍异常请检查实际设备宽度 |
+2
View File
@@ -0,0 +1,2 @@
android.overridePathCheck=true
android.useAndroidX=true
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+133
View File
@@ -0,0 +1,133 @@
#requires -version 5.1
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$ProjectDir,
[switch]$Force
)
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
function Resolve-FullPath {
param([Parameter(Mandatory = $true)][string]$Path)
return [IO.Path]::GetFullPath((Resolve-Path -LiteralPath $Path).Path)
}
function Assert-InsideDirectory {
param(
[Parameter(Mandatory = $true)][string]$Parent,
[Parameter(Mandatory = $true)][string]$Child
)
$parentFull = [IO.Path]::GetFullPath($Parent).TrimEnd([char[]]@('\', '/'))
$childFull = [IO.Path]::GetFullPath($Child).TrimEnd([char[]]@('\', '/'))
$parentPrefix = $parentFull + [IO.Path]::DirectorySeparatorChar
if ($childFull -eq $parentFull -or -not $childFull.StartsWith($parentPrefix, [StringComparison]::OrdinalIgnoreCase)) {
throw "Refusing to remove path outside project: $childFull"
}
}
function Stop-IfBuildToolsAreRunning {
$names = @('qtcreator', 'cmake', 'ninja', 'jom', 'mingw32-make', 'make')
$running = Get-Process -ErrorAction SilentlyContinue |
Where-Object { $names -contains $_.ProcessName.ToLowerInvariant() } |
Sort-Object ProcessName, Id
if ($running -and -not $Force) {
$summary = ($running | ForEach-Object { "$($_.ProcessName)#$($_.Id)" }) -join ', '
throw "Qt Creator or build tools are running: $summary. Close them first, or rerun with -Force if you are sure."
}
}
function Get-CacheTargets {
param([Parameter(Mandatory = $true)][string]$ProjectPath)
$literalNames = @(
'.qtcreator',
'.cmake',
'.qt',
'.qtc_clangd',
'build',
'out',
'CMakeCache.txt',
'CMakeFiles',
'cmake_install.cmake',
'compile_commands.json',
'CTestTestfile.cmake',
'DartConfiguration.tcl',
'install_manifest.txt',
'Makefile',
'CMakeUserPresets.json'
)
$wildcardFilters = @(
'build-*',
'cmake-build-*',
'*.user',
'*.user.*',
'*.autosave'
)
$items = New-Object 'System.Collections.Generic.List[System.IO.FileSystemInfo]'
foreach ($name in $literalNames) {
$path = Join-Path $ProjectPath $name
if (Test-Path -LiteralPath $path) {
$items.Add((Get-Item -LiteralPath $path -Force))
}
}
foreach ($filter in $wildcardFilters) {
Get-ChildItem -LiteralPath $ProjectPath -Force -Filter $filter -ErrorAction SilentlyContinue |
ForEach-Object { $items.Add($_) }
}
$seen = @{}
foreach ($item in $items) {
if (-not $seen.ContainsKey($item.FullName)) {
$seen[$item.FullName] = $true
$item
}
}
}
if ([string]::IsNullOrWhiteSpace($ProjectDir)) {
if (-not [string]::IsNullOrWhiteSpace($PSScriptRoot)) {
$ProjectDir = $PSScriptRoot
} elseif (-not [string]::IsNullOrWhiteSpace($PSCommandPath)) {
$ProjectDir = Split-Path -Parent $PSCommandPath
} else {
$ProjectDir = (Get-Location).Path
}
}
$projectPath = Resolve-FullPath -Path $ProjectDir
if (-not (Test-Path -LiteralPath (Join-Path $projectPath 'CMakeLists.txt'))) {
Write-Warning "CMakeLists.txt was not found in '$projectPath'. Use -ProjectDir to point at the Qt project root."
}
Stop-IfBuildToolsAreRunning
Write-Host "Project: $projectPath"
Write-Host 'Mode: delete current project Qt/CMake cache directly.'
$targets = @(Get-CacheTargets -ProjectPath $projectPath)
if ($targets.Count -eq 0) {
Write-Host 'No cache files found.'
exit 0
}
foreach ($target in $targets) {
Assert-InsideDirectory -Parent $projectPath -Child $target.FullName
if ($PSCmdlet.ShouldProcess($target.FullName, 'Remove Qt/CMake cache')) {
Remove-Item -LiteralPath $target.FullName -Recurse -Force
Write-Host "Removed: $($target.FullName)"
}
}
Write-Host 'Done.'
+1
View File
@@ -0,0 +1 @@
IDI_ICON1 ICON "@REWRITE_QT_APP_ICON@"
+7
View File
@@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Name=人脸识别考勤系统
Exec=apprewrite_qt
Icon=rewrite_qt
Terminal=false
Categories=Utility;
+63
View File
@@ -0,0 +1,63 @@
#include <QGuiApplication>
#include <QIcon>
#include <QLibraryInfo>
#include <QDir>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickStyle>
#include <QQuickWindow>
#include "src/AppController.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QIcon appIcon(QStringLiteral(":/assets/favicon.ico"));
appIcon.addFile(QStringLiteral(":/assets/favicon-256x256.png"));
appIcon.addFile(QStringLiteral(":/assets/favicon-192x192.png"));
appIcon.addFile(QStringLiteral(":/assets/favicon-128x128.png"));
appIcon.addFile(QStringLiteral(":/assets/favicon-64x64.png"));
appIcon.addFile(QStringLiteral(":/assets/favicon-48x48.png"));
appIcon.addFile(QStringLiteral(":/assets/favicon-32x32.png"));
appIcon.addFile(QStringLiteral(":/assets/favicon-16x16.png"));
app.setWindowIcon(appIcon);
QCoreApplication::setApplicationName("人脸识别考勤系统");
QCoreApplication::setApplicationVersion("3.0.0");
QCoreApplication::setOrganizationName("STM32 Face Recognition");
QQuickStyle::setStyle("Basic");
#if defined(Q_OS_ANDROID)
QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering);
#endif
AppController controller;
QQmlApplicationEngine engine;
const QString qmlImportPath = QLibraryInfo::path(QLibraryInfo::QmlImportsPath);
if (!qmlImportPath.isEmpty()) {
engine.addImportPath(qmlImportPath);
}
#if defined(REWRITE_QT_INSTALL_QML)
const QString qtInstallQmlPath = QStringLiteral(REWRITE_QT_INSTALL_QML);
if (QDir(qtInstallQmlPath).exists()) {
engine.addImportPath(qtInstallQmlPath);
}
#endif
engine.rootContext()->setContextProperty("appController", &controller);
const QUrl url(QStringLiteral("qrc:/qml/Main.qml"));
QObject::connect(
&engine,
&QQmlApplicationEngine::objectCreated,
&app,
[url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl) {
QCoreApplication::exit(-1);
}
},
Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
+3191
View File
File diff suppressed because it is too large Load Diff
+704
View File
@@ -0,0 +1,704 @@
#include "ApiClient.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QProcessEnvironment>
#include <QUrl>
#include <QUrlQuery>
#include "CryptoUtils.h"
namespace {
constexpr int NetworkTimeoutMs = 5000;
}
ApiClient::ApiClient(QObject *parent)
: QObject(parent)
, m_serverUrl("http://127.0.0.1:8082")
, m_apiToken(QProcessEnvironment::systemEnvironment()
.value(QStringLiteral("ATTENDANCE_API_TOKEN")))
{
}
void ApiClient::setServerUrl(const QString &url)
{
const QString trimmed = url.trimmed();
if (!trimmed.isEmpty()) {
m_serverUrl = trimmed;
}
}
QString ApiClient::serverUrl() const
{
return m_serverUrl;
}
void ApiClient::setApiToken(const QString &token)
{
const QString trimmed = token.trimmed();
m_apiToken = trimmed.isEmpty()
? QProcessEnvironment::systemEnvironment()
.value(QStringLiteral("ATTENDANCE_API_TOKEN"))
: trimmed;
}
QString ApiClient::apiToken() const
{
return m_apiToken;
}
QString ApiClient::buildUrl(const QString &endpoint, const QMap<QString, QString> &params) const
{
QString baseUrl = m_serverUrl.trimmed();
while (baseUrl.endsWith('/')) {
baseUrl.chop(1);
}
QString url = baseUrl + "/api" + endpoint;
if (!params.isEmpty()) {
QUrlQuery query;
for (auto it = params.begin(); it != params.end(); ++it) {
query.addQueryItem(it.key(), it.value());
}
url += "?" + query.toString(QUrl::FullyEncoded);
}
return url;
}
QNetworkRequest ApiClient::createRequest(const QString &url, bool encryptedResponse, bool jsonBody) const
{
QNetworkRequest request{QUrl(url)};
if (encryptedResponse) {
request.setRawHeader("X-Encrypted-Response", "true");
}
request.setRawHeader("X-Api-Token", m_apiToken.toUtf8());
request.setRawHeader("Accept", "application/json");
if (jsonBody) {
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
}
request.setTransferTimeout(NetworkTimeoutMs);
return request;
}
void ApiClient::fetchSyncState(qulonglong lastVersion)
{
const QString url = buildUrl("/v1/sync/state", {
{"client", "qt"},
{"last_version", QString::number(lastVersion)}
});
qInfo() << "[ApiClient] GET sync state" << url;
QNetworkReply *reply = m_networkManager.get(createRequest(url));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
emitNetworkError(QStringLiteral("同步状态加载失败"), reply);
reply->deleteLater();
return;
}
const QJsonObject root = QJsonDocument::fromJson(data).object();
const qulonglong version = static_cast<qulonglong>(root.value("version").toDouble(0));
const bool changed = root.value("changed").toBool(false);
if (!changed) {
emit syncStateFetched(version, false);
reply->deleteLater();
return;
}
const QJsonObject state = root.value("state").toObject();
if (state.contains("records")) {
emit recordsFetched(parseRecords(QJsonDocument(state.value("records").toArray()).toJson(QJsonDocument::Compact)));
}
if (state.contains("statistics")) {
const QJsonObject object = state.value("statistics").toObject();
emit statisticsFetched(object.value("total").toInt(),
object.value("today").toInt(),
object.value("server_time").toString());
}
if (state.contains("time_slots")) {
QVector<TimeSlot> timeSlots;
const QJsonArray array = state.value("time_slots").toArray();
for (const QJsonValue &value : array) {
const QJsonObject object = value.toObject();
timeSlots.push_back({
object.value("name").toString(),
QTime::fromString(object.value("start_time").toString(), "HH:mm"),
QTime::fromString(object.value("end_time").toString(), "HH:mm"),
object.value("enabled").toBool(true)
});
}
emit timeSlotsFetched(timeSlots);
}
if (state.contains("mapping_config")) {
const QJsonObject mappingConfig = state.value("mapping_config").toObject();
QMap<QString, QString> studentMap;
QMap<QString, QString> studentClassMap;
QMap<QString, QString> classMap;
const QJsonObject studentObject = mappingConfig.value("student_name_map").toObject();
for (auto it = studentObject.begin(); it != studentObject.end(); ++it) {
studentMap[it.key()] = it.value().toString();
}
const QJsonObject studentClassObject = mappingConfig.value("student_class_map").toObject();
for (auto it = studentClassObject.begin(); it != studentClassObject.end(); ++it) {
studentClassMap[it.key()] = it.value().toString();
}
const QJsonObject classObject = mappingConfig.value("class_name_map").toObject();
for (auto it = classObject.begin(); it != classObject.end(); ++it) {
classMap[it.key()] = it.value().toString();
}
emit mappingConfigFetched(studentMap, studentClassMap, classMap);
}
if (state.contains("face_mappings")) {
const QJsonObject mappings = state.value("face_mappings").toObject();
QMap<QString, QString> mapping;
for (auto it = mappings.begin(); it != mappings.end(); ++it) {
mapping[it.key()] = it.value().toString();
}
emit faceIdMappingFetched(mapping);
}
if (state.contains("face_photos")) {
QStringList ids;
const QJsonArray photos = state.value("face_photos").toArray();
for (const QJsonValue &value : photos) {
ids.push_back(value.toObject().value("id").toString());
}
emit facePhotosFetched(ids);
}
if (state.contains("all_student_ids")) {
QStringList ids;
const QJsonArray array = state.value("all_student_ids").toArray();
for (const QJsonValue &value : array) {
ids.push_back(value.toString());
}
emit allStudentIdsFetched(ids);
}
emit syncStateFetched(version, true);
reply->deleteLater();
});
}
QByteArray ApiClient::maybeDecrypt(const QByteArray &data, bool encrypted) const
{
if (!encrypted) {
return data;
}
const QByteArray trimmed = data.trimmed();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
return trimmed;
}
return CryptoUtils::decryptAes128CbcBase64(trimmed);
}
bool ApiClient::isConnectionError(QNetworkReply::NetworkError error) const
{
switch (error) {
case QNetworkReply::ConnectionRefusedError:
case QNetworkReply::RemoteHostClosedError:
case QNetworkReply::HostNotFoundError:
case QNetworkReply::TimeoutError:
case QNetworkReply::OperationCanceledError:
case QNetworkReply::SslHandshakeFailedError:
case QNetworkReply::TemporaryNetworkFailureError:
case QNetworkReply::NetworkSessionFailedError:
case QNetworkReply::BackgroundRequestNotAllowedError:
case QNetworkReply::TooManyRedirectsError:
case QNetworkReply::InsecureRedirectError:
case QNetworkReply::UnknownNetworkError:
case QNetworkReply::UnknownProxyError:
case QNetworkReply::UnknownContentError:
case QNetworkReply::ProtocolFailure:
case QNetworkReply::UnknownServerError:
return true;
default:
return false;
}
}
void ApiClient::emitNetworkError(const QString &prefix, QNetworkReply *reply)
{
emit networkError(prefix);
if (isConnectionError(reply->error())) {
emit connectionUnavailable(prefix);
}
}
QVector<AttendanceRecord> ApiClient::parseRecords(const QByteArray &data) const
{
QVector<AttendanceRecord> records;
const QJsonDocument doc = QJsonDocument::fromJson(data);
if (!doc.isArray()) {
return records;
}
const QJsonArray array = doc.array();
records.reserve(array.size());
for (const QJsonValue &value : array) {
const QJsonObject object = value.toObject();
AttendanceRecord record;
record.timestamp = object.value("timestamp").toString().trimmed();
record.studentId = object.value("student_id").toString().trimmed();
record.originalName = object.value("name").toString().trimmed();
record.confidence = object.value("confidence").toDouble();
const QJsonValue statusValue = object.value("status");
if (statusValue.isBool()) {
record.status = statusValue.toBool() ? QStringLiteral("success") : QStringLiteral("failed");
} else if (statusValue.isDouble()) {
record.status = QString::number(statusValue.toInt());
} else {
record.status = statusValue.toString().trimmed();
}
record.classId = object.value("class_id").toString().trimmed();
if (record.classId.isEmpty()) {
record.classId = "0";
}
if (!record.timestamp.startsWith("0000-") && !record.timestamp.isEmpty() && !record.studentId.isEmpty()) {
records.push_back(record);
}
}
return records;
}
void ApiClient::fetchRecords(int limit, int offset)
{
const QString url = buildUrl("/v1/attendance/records", {
{"limit", QString::number(limit)},
{"offset", QString::number(offset)}
});
qInfo() << "[ApiClient] GET" << url;
QNetworkReply *reply = m_networkManager.get(createRequest(url));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray rawData = reply->readAll();
const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qInfo() << "[ApiClient] records status=" << statusCode
<< "error=" << reply->error()
<< "bytes=" << rawData.size()
<< "body=" << rawData.left(200);
if (reply->error() != QNetworkReply::NoError) {
emitNetworkError(QStringLiteral("加载记录失败"), reply);
reply->deleteLater();
return;
}
const QByteArray data = maybeDecrypt(rawData, false);
if (data.isEmpty()) {
emit networkError("记录数据为空");
reply->deleteLater();
return;
}
QJsonParseError jsonError{};
const QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if (jsonError.error != QJsonParseError::NoError || !doc.isArray()) {
emit networkError(QStringLiteral("记录数据解析失败: %1").arg(jsonError.errorString()));
reply->deleteLater();
return;
}
const QVector<AttendanceRecord> records = parseRecords(data);
qInfo() << "[ApiClient] parsed records=" << records.size();
emit recordsFetched(records);
reply->deleteLater();
});
}
void ApiClient::fetchStatistics()
{
const QString url = buildUrl("/v1/attendance/statistics");
qInfo() << "[ApiClient] GET" << url;
QNetworkReply *reply = m_networkManager.get(createRequest(url));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray rawData = reply->readAll();
const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qInfo() << "[ApiClient] statistics status=" << statusCode
<< "error=" << reply->error()
<< "bytes=" << rawData.size()
<< "body=" << rawData.left(200);
if (reply->error() != QNetworkReply::NoError) {
emitNetworkError(QStringLiteral("加载统计失败"), reply);
reply->deleteLater();
return;
}
const QByteArray data = maybeDecrypt(rawData, false);
if (data.isEmpty()) {
emit networkError("统计数据为空");
reply->deleteLater();
return;
}
const QJsonDocument doc = QJsonDocument::fromJson(data);
const QJsonObject object = doc.object();
emit statisticsFetched(object.value("total").toInt(),
object.value("today").toInt(),
object.value("server_time").toString());
reply->deleteLater();
});
}
void ApiClient::clearAllRecords()
{
QNetworkReply *reply = m_networkManager.post(createRequest(buildUrl("/v1/attendance/records/clear"), false, true), QByteArray());
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
if (isConnectionError(reply->error())) {
emit connectionUnavailable(QStringLiteral("清空失败: %1").arg(reply->errorString()));
}
emit recordsCleared(false, reply->errorString());
reply->deleteLater();
return;
}
const QJsonObject object = QJsonDocument::fromJson(data).object();
emit recordsCleared(object.value("success").toBool(), object.value("message").toString());
reply->deleteLater();
});
}
void ApiClient::deleteRecord(const QString &timestamp, const QString &studentId)
{
QNetworkReply *reply = m_networkManager.deleteResource(createRequest(buildUrl("/v1/attendance/records", {
{"timestamp", timestamp},
{"student_id", studentId}
})));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
if (isConnectionError(reply->error())) {
emit connectionUnavailable(QStringLiteral("记录删除失败: %1").arg(reply->errorString()));
}
emit recordDeleted(false, reply->errorString());
reply->deleteLater();
return;
}
const QJsonObject object = QJsonDocument::fromJson(data).object();
emit recordDeleted(object.value("success").toBool(), object.value("message").toString());
reply->deleteLater();
});
}
void ApiClient::fetchTimeSlots()
{
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/config/time-slots")));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
emitNetworkError(QStringLiteral("加载时间段失败"), reply);
reply->deleteLater();
return;
}
QVector<TimeSlot> timeSlots;
const QJsonObject root = QJsonDocument::fromJson(data).object();
const QJsonArray array = root.value("time_slots").toArray();
for (const QJsonValue &value : array) {
const QJsonObject object = value.toObject();
timeSlots.push_back({
object.value("name").toString(),
QTime::fromString(object.value("start_time").toString(), "HH:mm"),
QTime::fromString(object.value("end_time").toString(), "HH:mm"),
object.value("enabled").toBool(true)
});
}
emit timeSlotsFetched(timeSlots);
reply->deleteLater();
});
}
void ApiClient::uploadTimeSlots(const QVector<TimeSlot> &timeSlots)
{
QJsonArray array;
for (const TimeSlot &slot : timeSlots) {
QJsonObject object;
object["name"] = slot.name;
object["start_time"] = slot.startTime.toString("HH:mm");
object["end_time"] = slot.endTime.toString("HH:mm");
object["enabled"] = slot.enabled;
array.append(object);
}
QJsonObject payload;
payload["time_slots"] = array;
const QByteArray body = QJsonDocument(payload).toJson(QJsonDocument::Compact);
qInfo() << "[ApiClient] POST" << buildUrl("/v1/config/time-slots")
<< "bytes=" << body.size()
<< "body=" << body.left(300);
QNetworkReply *reply = m_networkManager.post(createRequest(buildUrl("/v1/config/time-slots"), false, true), body);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
if (isConnectionError(reply->error())) {
emit connectionUnavailable(QStringLiteral("时间段同步失败: %1").arg(reply->errorString()));
}
emit timeSlotsUploaded(false, reply->errorString());
reply->deleteLater();
return;
}
const QJsonObject object = QJsonDocument::fromJson(data).object();
emit timeSlotsUploaded(object.value("success").toBool(), object.value("message").toString());
reply->deleteLater();
});
}
void ApiClient::fetchMappingConfig()
{
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/config/mappings")));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
emitNetworkError(QStringLiteral("加载映射配置失败"), reply);
reply->deleteLater();
return;
}
const QJsonObject root = QJsonDocument::fromJson(data).object();
QMap<QString, QString> studentMap;
QMap<QString, QString> studentClassMap;
QMap<QString, QString> classMap;
const QJsonObject studentObject = root.value("student_name_map").toObject();
for (auto it = studentObject.begin(); it != studentObject.end(); ++it) {
studentMap[it.key()] = it.value().toString();
}
const QJsonObject studentClassObject = root.value("student_class_map").toObject();
for (auto it = studentClassObject.begin(); it != studentClassObject.end(); ++it) {
studentClassMap[it.key()] = it.value().toString();
}
const QJsonObject classObject = root.value("class_name_map").toObject();
for (auto it = classObject.begin(); it != classObject.end(); ++it) {
classMap[it.key()] = it.value().toString();
}
emit mappingConfigFetched(studentMap, studentClassMap, classMap);
reply->deleteLater();
});
}
void ApiClient::uploadMappingConfig(const QMap<QString, QString> &studentNameMap,
const QMap<QString, QString> &studentClassMap,
const QMap<QString, QString> &classNameMap)
{
QJsonObject studentObject;
for (auto it = studentNameMap.begin(); it != studentNameMap.end(); ++it) {
studentObject[it.key()] = it.value();
}
QJsonObject studentClassObject;
for (auto it = studentClassMap.begin(); it != studentClassMap.end(); ++it) {
studentClassObject[it.key()] = it.value();
}
QJsonObject classObject;
for (auto it = classNameMap.begin(); it != classNameMap.end(); ++it) {
classObject[it.key()] = it.value();
}
QJsonObject payload;
payload["student_name_map"] = studentObject;
payload["student_class_map"] = studentClassObject;
payload["class_name_map"] = classObject;
QNetworkReply *reply = m_networkManager.post(createRequest(buildUrl("/v1/config/mappings"), false, true),
QJsonDocument(payload).toJson(QJsonDocument::Compact));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
if (isConnectionError(reply->error())) {
emit connectionUnavailable(QStringLiteral("映射保存失败: %1").arg(reply->errorString()));
}
emit mappingConfigUploaded(false, reply->errorString());
reply->deleteLater();
return;
}
const QJsonObject object = QJsonDocument::fromJson(data).object();
emit mappingConfigUploaded(object.value("success").toBool(), object.value("message").toString());
reply->deleteLater();
});
}
void ApiClient::fetchFaceIdMapping()
{
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/faces/mappings")));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
emitNetworkError(QStringLiteral("加载人脸映射失败"), reply);
reply->deleteLater();
return;
}
const QJsonObject mappings = QJsonDocument::fromJson(data).object().value("mappings").toObject();
QMap<QString, QString> mapping;
for (auto it = mappings.begin(); it != mappings.end(); ++it) {
mapping[it.key()] = it.value().toString();
}
emit faceIdMappingFetched(mapping);
reply->deleteLater();
});
}
void ApiClient::uploadFaceIdMapping(const QMap<QString, QString> &mapping)
{
QJsonObject mappings;
for (auto it = mapping.begin(); it != mapping.end(); ++it) {
if (!it.value().trimmed().isEmpty()) {
mappings[it.key()] = it.value();
}
}
QJsonObject payload;
payload["mappings"] = mappings;
QNetworkReply *reply = m_networkManager.post(createRequest(buildUrl("/v1/faces/mappings"), false, true),
QJsonDocument(payload).toJson(QJsonDocument::Compact));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
if (isConnectionError(reply->error())) {
emit connectionUnavailable(QStringLiteral("人脸映射保存失败: %1").arg(reply->errorString()));
}
emit faceIdMappingUploaded(false, reply->errorString());
reply->deleteLater();
return;
}
const QJsonObject object = QJsonDocument::fromJson(data).object();
emit faceIdMappingUploaded(object.value("success").toBool(), object.value("message").toString());
reply->deleteLater();
});
}
void ApiClient::fetchFacePhotos()
{
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/faces/photos")));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
emitNetworkError(QStringLiteral("加载照片列表失败"), reply);
reply->deleteLater();
return;
}
QStringList ids;
const QJsonArray photos = QJsonDocument::fromJson(data).object().value("photos").toArray();
for (const QJsonValue &value : photos) {
ids.push_back(value.toObject().value("id").toString());
}
emit facePhotosFetched(ids);
reply->deleteLater();
});
}
void ApiClient::fetchFacePhoto(const QString &faceId)
{
if (faceId.trimmed().isEmpty()) {
emit facePhotoFetched(faceId, {}, false, QStringLiteral("人脸ID为空"));
return;
}
const QString encodedId = QString::fromUtf8(QUrl::toPercentEncoding(faceId.trimmed()));
QNetworkRequest request = createRequest(buildUrl(QStringLiteral("/v1/faces/photos/%1").arg(encodedId)));
request.setRawHeader("Accept", "image/jpeg,*/*");
QNetworkReply *reply = m_networkManager.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply, faceId]() {
const QByteArray data = reply->readAll();
const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (reply->error() != QNetworkReply::NoError || statusCode >= 400 || data.isEmpty()) {
const QString message = reply->error() != QNetworkReply::NoError
? reply->errorString()
: QStringLiteral("HTTP %1").arg(statusCode);
emit facePhotoFetched(faceId, {}, false, message);
reply->deleteLater();
return;
}
emit facePhotoFetched(faceId, data, true, QString());
reply->deleteLater();
});
}
void ApiClient::fetchAllStudentIds()
{
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/faces/student-ids")));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
emitNetworkError(QStringLiteral("加载学生ID失败"), reply);
reply->deleteLater();
return;
}
QStringList ids;
const QJsonArray array = QJsonDocument::fromJson(data).object().value("student_ids").toArray();
for (const QJsonValue &value : array) {
ids.push_back(value.toString());
}
emit allStudentIdsFetched(ids);
reply->deleteLater();
});
}
void ApiClient::deleteFace(const QString &studentId, bool deleteRecords)
{
QString baseUrl = m_serverUrl.trimmed();
while (baseUrl.endsWith('/')) {
baseUrl.chop(1);
}
QString url = baseUrl + "/api/v1/faces/" + QString::fromLatin1(QUrl::toPercentEncoding(studentId));
if (deleteRecords) {
url += "?delete_records=true";
}
QNetworkReply *reply = m_networkManager.deleteResource(createRequest(url));
connect(reply, &QNetworkReply::finished, this, [this, reply, studentId]() {
const QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
if (isConnectionError(reply->error())) {
emit connectionUnavailable(QStringLiteral("删除失败: %1").arg(reply->errorString()));
}
emit faceDeleted(false, studentId, 0, reply->errorString());
reply->deleteLater();
return;
}
const QJsonObject object = QJsonDocument::fromJson(data).object();
emit faceDeleted(object.value("success").toBool(),
studentId,
object.value("records_deleted").toInt(),
object.value("message").toString());
reply->deleteLater();
});
}
void ApiClient::checkHealth()
{
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/system/health")));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
emit healthChecked(false, reply->errorString());
reply->deleteLater();
return;
}
const QJsonObject object = QJsonDocument::fromJson(data).object();
const bool healthy = object.value("status").toString() == "healthy";
emit healthChecked(healthy, healthy ? QStringLiteral("连接成功") : QStringLiteral("服务器无响应"));
reply->deleteLater();
});
}
+80
View File
@@ -0,0 +1,80 @@
#ifndef API_CLIENT_H
#define API_CLIENT_H
#include <QObject>
#include <QByteArray>
#include <QMap>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QStringList>
#include <QVector>
#include "AttendanceTypes.h"
class ApiClient : public QObject
{
Q_OBJECT
public:
explicit ApiClient(QObject *parent = nullptr);
void setServerUrl(const QString &url);
QString serverUrl() const;
void setApiToken(const QString &token);
QString apiToken() const;
void fetchSyncState(qulonglong lastVersion);
void fetchRecords(int limit = 1000, int offset = 0);
void fetchStatistics();
void clearAllRecords();
void deleteRecord(const QString &timestamp, const QString &studentId);
void fetchTimeSlots();
void uploadTimeSlots(const QVector<TimeSlot> &timeSlots);
void fetchMappingConfig();
void uploadMappingConfig(const QMap<QString, QString> &studentNameMap,
const QMap<QString, QString> &studentClassMap,
const QMap<QString, QString> &classNameMap);
void fetchFaceIdMapping();
void uploadFaceIdMapping(const QMap<QString, QString> &mapping);
void fetchFacePhotos();
void fetchFacePhoto(const QString &faceId);
void fetchAllStudentIds();
void deleteFace(const QString &studentId, bool deleteRecords);
void checkHealth();
signals:
void recordsFetched(const QVector<AttendanceRecord> &records);
void statisticsFetched(int total, int today, const QString &serverTime);
void recordsCleared(bool success, const QString &message);
void recordDeleted(bool success, const QString &message);
void timeSlotsFetched(const QVector<TimeSlot> &timeSlots);
void timeSlotsUploaded(bool success, const QString &message);
void mappingConfigFetched(const QMap<QString, QString> &studentNameMap,
const QMap<QString, QString> &studentClassMap,
const QMap<QString, QString> &classNameMap);
void mappingConfigUploaded(bool success, const QString &message);
void faceIdMappingFetched(const QMap<QString, QString> &mapping);
void faceIdMappingUploaded(bool success, const QString &message);
void facePhotosFetched(const QStringList &photoIds);
void facePhotoFetched(const QString &faceId, const QByteArray &data, bool success, const QString &message);
void allStudentIdsFetched(const QStringList &studentIds);
void faceDeleted(bool success, const QString &studentId, int recordsDeleted, const QString &message);
void healthChecked(bool healthy, const QString &message);
void connectionUnavailable(const QString &message);
void networkError(const QString &message);
void syncStateFetched(qulonglong version, bool changed);
private:
QString buildUrl(const QString &endpoint, const QMap<QString, QString> &params = {}) const;
QNetworkRequest createRequest(const QString &url, bool encryptedResponse = false, bool jsonBody = false) const;
QVector<AttendanceRecord> parseRecords(const QByteArray &data) const;
QByteArray maybeDecrypt(const QByteArray &data, bool encrypted) const;
void emitNetworkError(const QString &prefix, QNetworkReply *reply);
bool isConnectionError(QNetworkReply::NetworkError error) const;
QNetworkAccessManager m_networkManager;
QString m_serverUrl;
QString m_apiToken;
};
#endif
File diff suppressed because it is too large Load Diff
+201
View File
@@ -0,0 +1,201 @@
#ifndef APP_CONTROLLER_H
#define APP_CONTROLLER_H
#include <QDate>
#include <QHash>
#include <QMap>
#include <QObject>
#include <QStringList>
#include <QTimer>
#include <QVector>
#include "ApiClient.h"
#include "FaceListModel.h"
#include "MappingListModel.h"
#include "RecordListModel.h"
#include "StatisticsListModel.h"
#include "StudentProfileListModel.h"
#include "TimeSlotListModel.h"
class AppController : public QObject
{
Q_OBJECT
Q_PROPERTY(RecordListModel* recordModel READ recordModel CONSTANT)
Q_PROPERTY(StatisticsListModel* statisticsModel READ statisticsModel CONSTANT)
Q_PROPERTY(TimeSlotListModel* timeSlotModel READ timeSlotModel CONSTANT)
Q_PROPERTY(StudentProfileListModel* studentProfileModel READ studentProfileModel CONSTANT)
Q_PROPERTY(MappingListModel* studentMappingModel READ studentMappingModel CONSTANT)
Q_PROPERTY(MappingListModel* studentClassMappingModel READ studentClassMappingModel CONSTANT)
Q_PROPERTY(MappingListModel* classMappingModel READ classMappingModel CONSTANT)
Q_PROPERTY(FaceListModel* faceModel READ faceModel CONSTANT)
Q_PROPERTY(QStringList classOptions READ classOptions NOTIFY filtersChanged)
Q_PROPERTY(QStringList studentOptions READ studentOptions NOTIFY filtersChanged)
Q_PROPERTY(QStringList classSelectionOptions READ classSelectionOptions NOTIFY studentProfilesChanged)
Q_PROPERTY(QString serverUrl READ serverUrl WRITE setServerUrl NOTIFY serverUrlChanged)
Q_PROPERTY(QString apiToken READ apiToken WRITE setApiToken NOTIFY apiTokenChanged)
Q_PROPERTY(QString studentProfileSearchText READ studentProfileSearchText WRITE setStudentProfileSearchText NOTIFY studentProfileFilterChanged)
Q_PROPERTY(QString studentProfileClassFilter READ studentProfileClassFilter WRITE setStudentProfileClassFilter NOTIFY studentProfileFilterChanged)
Q_PROPERTY(QString studentProfileBindingFilter READ studentProfileBindingFilter WRITE setStudentProfileBindingFilter NOTIFY studentProfileFilterChanged)
Q_PROPERTY(QStringList studentProfileClassFilterOptions READ studentProfileClassFilterOptions NOTIFY studentProfilesChanged)
Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusMessageChanged)
Q_PROPERTY(bool reconnecting READ reconnecting NOTIFY reconnectStateChanged)
Q_PROPERTY(int reconnectAttempt READ reconnectAttempt NOTIFY reconnectStateChanged)
Q_PROPERTY(int totalRecords READ totalRecords NOTIFY summaryChanged)
Q_PROPERTY(int todayRecords READ todayRecords NOTIFY summaryChanged)
Q_PROPERTY(int filteredCount READ filteredCount NOTIFY summaryChanged)
Q_PROPERTY(int loadedRecords READ loadedRecords NOTIFY summaryChanged)
Q_PROPERTY(int statisticsStudentCount READ statisticsStudentCount NOTIFY statisticsChanged)
Q_PROPERTY(int statisticsRecordCount READ statisticsRecordCount NOTIFY statisticsChanged)
Q_PROPERTY(QString statisticsRangeText READ statisticsRangeText NOTIFY statisticsChanged)
public:
explicit AppController(QObject *parent = nullptr);
RecordListModel *recordModel();
StatisticsListModel *statisticsModel();
TimeSlotListModel *timeSlotModel();
StudentProfileListModel *studentProfileModel();
MappingListModel *studentMappingModel();
MappingListModel *studentClassMappingModel();
MappingListModel *classMappingModel();
FaceListModel *faceModel();
QStringList classOptions() const;
QStringList studentOptions() const;
QStringList classSelectionOptions() const;
QString serverUrl() const;
QString apiToken() const;
QString studentProfileSearchText() const;
QString studentProfileClassFilter() const;
QString studentProfileBindingFilter() const;
QStringList studentProfileClassFilterOptions() const;
QString statusMessage() const;
bool reconnecting() const;
int reconnectAttempt() const;
int totalRecords() const;
int todayRecords() const;
int filteredCount() const;
int loadedRecords() const;
int statisticsStudentCount() const;
int statisticsRecordCount() const;
QString statisticsRangeText() const;
void setServerUrl(const QString &url);
void setApiToken(const QString &token);
void setStudentProfileSearchText(const QString &text);
void setStudentProfileClassFilter(const QString &classFilter);
void setStudentProfileBindingFilter(const QString &bindingFilter);
Q_INVOKABLE void refreshAll();
Q_INVOKABLE void refreshRecords();
Q_INVOKABLE void applyFilters(const QString &classId,
const QString &studentId,
const QString &nameFilter,
int dateFilterIndex,
const QString &specificDate);
Q_INVOKABLE void updateStatisticsRange(const QString &startDate, const QString &endDate);
Q_INVOKABLE bool exportFilteredRecords(const QString &fileUrl);
Q_INVOKABLE bool exportStatistics(const QString &fileUrl);
Q_INVOKABLE void clearAllRecords();
Q_INVOKABLE void saveTimeSlots();
Q_INVOKABLE void reloadTimeSlotsFromServer();
Q_INVOKABLE void loadMappingsFromServer();
Q_INVOKABLE void saveMappings();
Q_INVOKABLE void addStudentProfile();
Q_INVOKABLE void updateStudentProfile(int index,
const QString &studentId,
const QString &studentName,
const QString &classId);
Q_INVOKABLE void removeStudentProfile(int index);
Q_INVOKABLE QString classDisplayForId(const QString &classId) const;
Q_INVOKABLE QString classIdFromDisplay(const QString &display) const;
Q_INVOKABLE void refreshFaces();
Q_INVOKABLE void updateFaceStudentId(int index, const QString &studentId);
Q_INVOKABLE void saveFaceMappings();
Q_INVOKABLE void deleteFace(const QString &studentId, bool deleteRecords);
Q_INVOKABLE void testConnection();
Q_INVOKABLE void saveServerConfig(const QString &url, const QString &token);
signals:
void filtersChanged();
void serverUrlChanged();
void apiTokenChanged();
void statusMessageChanged();
void reconnectStateChanged();
void summaryChanged();
void statisticsChanged();
void studentProfilesChanged();
void studentProfileFilterChanged();
private:
void loadLocalState();
void saveServerUrl();
void saveApiToken();
void saveMappingsToLocal();
void loadMappingsFromLocal();
void saveTimeSlotsToLocal();
void loadTimeSlotsFromLocal();
QString localSettingsPath(const QString &fileName) const;
void updateFilterOptions();
void rebuildStudentProfiles();
void applyStudentProfilesToMappings();
bool validateMappings(QString *message) const;
bool validateFaceMappings(QString *message) const;
QString faceBindingTextForStudent(const QString &studentId) const;
void rebuildFilteredRecords();
void rebuildStatistics();
QString mappedStudentName(const AttendanceRecord &record) const;
QString effectiveClassId(const AttendanceRecord &record) const;
QString mappedClassName(const AttendanceRecord &record) const;
QString localFacePhotoPath(const QString &faceId) const;
void mergeFaceItems();
void setStatusMessage(const QString &message);
void startReconnect(const QString &reason);
void stopReconnect();
void scheduleReconnect();
void runReconnectCheck();
ApiClient m_apiClient;
QTimer m_reconnectTimer;
RecordListModel m_recordModel;
StatisticsListModel m_statisticsModel;
TimeSlotListModel m_timeSlotModel;
StudentProfileListModel m_studentProfileModel;
MappingListModel m_studentMappingModel;
MappingListModel m_studentClassMappingModel;
MappingListModel m_classMappingModel;
FaceListModel m_faceModel;
QVector<AttendanceRecord> m_allRecords;
QVector<AttendanceRecord> m_filteredRecords;
QStringList m_classOptions;
QStringList m_studentOptions;
QString m_serverUrl;
QString m_apiToken;
QString m_studentProfileSearchText;
QString m_studentProfileClassFilter = "全部";
QString m_studentProfileBindingFilter = "全部";
QString m_statusMessage;
bool m_reconnecting = false;
bool m_reconnectCheckInFlight = false;
int m_reconnectAttempt = 0;
int m_totalRecords = 0;
int m_todayRecords = 0;
QString m_selectedClassId = "全部";
QString m_selectedStudentId = "全部";
QString m_nameFilter;
int m_dateFilterIndex = 0;
QDate m_specificDate = QDate::currentDate();
QDate m_statisticsStartDate = QDate::currentDate().addDays(-30);
QDate m_statisticsEndDate = QDate::currentDate();
int m_statisticsStudentCount = 0;
int m_statisticsRecordCount = 0;
qulonglong m_qtSyncVersion = 0;
QMap<QString, QString> m_faceIdMapping;
QHash<QString, QString> m_facePhotoLocalUrls;
QStringList m_facePhotoIds;
};
#endif
+54
View File
@@ -0,0 +1,54 @@
#ifndef ATTENDANCE_TYPES_H
#define ATTENDANCE_TYPES_H
#include <QDate>
#include <QDateTime>
#include <QString>
#include <QTime>
#include <QVector>
struct AttendanceRecord
{
QString timestamp;
QString studentId;
QString originalName;
double confidence = 0.0;
QString status;
QString classId = "0";
QDateTime dateTime() const
{
return QDateTime::fromString(timestamp, "yyyy-MM-dd HH:mm:ss");
}
};
struct StudentStatistics
{
QString studentId;
QString name;
int totalCount = 0;
int todayCount = 0;
QDateTime lastCheckIn;
double averageConfidence = 0.0;
};
struct TimeSlot
{
QString name;
QTime startTime;
QTime endTime;
bool enabled = true;
};
inline QVector<TimeSlot> defaultTimeSlots()
{
return {};
}
Q_DECLARE_METATYPE(AttendanceRecord)
Q_DECLARE_METATYPE(StudentStatistics)
Q_DECLARE_METATYPE(TimeSlot)
Q_DECLARE_METATYPE(QVector<AttendanceRecord>)
Q_DECLARE_METATYPE(QVector<TimeSlot>)
#endif
+48
View File
@@ -0,0 +1,48 @@
#include "CryptoUtils.h"
#include "crypto/tiny_aes.hpp"
namespace {
QByteArray removePkcs7Padding(const QByteArray &data)
{
if (data.isEmpty()) {
return {};
}
const int padding = static_cast<unsigned char>(data.back());
if (padding < 1 || padding > AES_BLOCKLEN || padding > data.size()) {
return data;
}
for (int i = data.size() - padding; i < data.size(); ++i) {
if (static_cast<unsigned char>(data.at(i)) != padding) {
return data;
}
}
return data.left(data.size() - padding);
}
}
QByteArray CryptoUtils::decryptAes128CbcBase64(const QByteArray &encryptedBase64)
{
static const uint8_t key[AES_KEYLEN] = {
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c
};
static const uint8_t iv[AES_BLOCKLEN] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
};
const QByteArray encrypted = QByteArray::fromBase64(encryptedBase64);
if (encrypted.isEmpty() || (encrypted.size() % AES_BLOCKLEN) != 0) {
return {};
}
QByteArray buffer = encrypted;
AES_ctx ctx;
AES_init_ctx_iv(&ctx, key, iv);
AES_CBC_decrypt_buffer(&ctx, reinterpret_cast<uint8_t*>(buffer.data()), static_cast<size_t>(buffer.size()));
return removePkcs7Padding(buffer);
}
+12
View File
@@ -0,0 +1,12 @@
#ifndef CRYPTO_UTILS_H
#define CRYPTO_UTILS_H
#include <QByteArray>
class CryptoUtils
{
public:
static QByteArray decryptAes128CbcBase64(const QByteArray &encryptedBase64);
};
#endif
+93
View File
@@ -0,0 +1,93 @@
#include "FaceListModel.h"
FaceListModel::FaceListModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int FaceListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_items.size();
}
QVariant FaceListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size()) {
return {};
}
const Item &item = m_items.at(index.row());
switch (role) {
case FaceIdRole: return item.faceId;
case StudentIdRole: return item.studentId;
case HasPhotoRole: return item.hasPhoto;
case PhotoUrlRole: return item.photoUrl;
case StudentNameRole: return item.studentName;
case ClassIdRole: return item.classId;
case ClassNameRole: return item.className;
case BindingStatusRole: return item.bindingStatus;
default: return {};
}
}
bool FaceListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size()) {
return false;
}
if (role == StudentIdRole) {
m_items[index.row()].studentId = value.toString().trimmed();
emit dataChanged(index, index, {role});
return true;
}
return false;
}
Qt::ItemFlags FaceListModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}
QHash<int, QByteArray> FaceListModel::roleNames() const
{
return {
{FaceIdRole, "faceId"},
{StudentIdRole, "studentId"},
{HasPhotoRole, "hasPhoto"},
{PhotoUrlRole, "photoUrl"},
{StudentNameRole, "studentName"},
{ClassIdRole, "classId"},
{ClassNameRole, "className"},
{BindingStatusRole, "bindingStatus"}
};
}
void FaceListModel::setItems(const QVector<Item> &items)
{
beginResetModel();
m_items = items;
endResetModel();
}
QVector<FaceListModel::Item> FaceListModel::items() const
{
return m_items;
}
void FaceListModel::updateStudentId(int index, const QString &studentId)
{
if (index < 0 || index >= m_items.size()) {
return;
}
m_items[index].studentId = studentId.trimmed();
emit dataChanged(this->index(index), this->index(index), {StudentIdRole});
}
+55
View File
@@ -0,0 +1,55 @@
#ifndef FACE_LIST_MODEL_H
#define FACE_LIST_MODEL_H
#include <QAbstractListModel>
#include <QByteArray>
#include <QHash>
#include <QModelIndex>
#include <QString>
#include <QVariant>
#include <QVector>
class FaceListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
FaceIdRole = Qt::UserRole + 1,
StudentIdRole,
HasPhotoRole,
PhotoUrlRole,
StudentNameRole,
ClassIdRole,
ClassNameRole,
BindingStatusRole
};
explicit FaceListModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QHash<int, QByteArray> roleNames() const override;
struct Item {
QString faceId;
QString studentId;
bool hasPhoto = false;
QString photoUrl;
QString studentName;
QString classId;
QString className;
QString bindingStatus;
};
void setItems(const QVector<Item> &items);
QVector<Item> items() const;
Q_INVOKABLE void updateStudentId(int index, const QString &studentId);
private:
QVector<Item> m_items;
};
#endif
+132
View File
@@ -0,0 +1,132 @@
#include "MappingListModel.h"
#include <algorithm>
MappingListModel::MappingListModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int MappingListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_items.size();
}
QVariant MappingListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size()) {
return {};
}
const Item &item = m_items.at(index.row());
switch (role) {
case KeyRole: return item.key;
case ValueRole: return item.value;
default: return {};
}
}
bool MappingListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size()) {
return false;
}
Item &item = m_items[index.row()];
switch (role) {
case KeyRole: item.key = value.toString().trimmed(); break;
case ValueRole: item.value = value.toString().trimmed(); break;
default: return false;
}
emit dataChanged(index, index, {role});
return true;
}
Qt::ItemFlags MappingListModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}
QHash<int, QByteArray> MappingListModel::roleNames() const
{
return {
{KeyRole, "entryKey"},
{ValueRole, "entryValue"}
};
}
void MappingListModel::setMap(const QMap<QString, QString> &map)
{
QVector<Item> items;
items.reserve(map.size());
for (auto it = map.begin(); it != map.end(); ++it) {
items.push_back({it.key(), it.value()});
}
std::sort(items.begin(), items.end(), [](const Item &left, const Item &right) {
bool leftNumeric = false;
bool rightNumeric = false;
int leftValue = left.key.toInt(&leftNumeric);
int rightValue = right.key.toInt(&rightNumeric);
if (leftNumeric && rightNumeric) {
return leftValue < rightValue;
}
return left.key < right.key;
});
beginResetModel();
m_items = items;
endResetModel();
}
QMap<QString, QString> MappingListModel::toMap() const
{
QMap<QString, QString> map;
for (const Item &item : m_items) {
if (!item.key.trimmed().isEmpty()) {
map[item.key.trimmed()] = item.value.trimmed();
}
}
return map;
}
void MappingListModel::addEntry(const QString &key, const QString &value)
{
beginInsertRows(QModelIndex(), m_items.size(), m_items.size());
m_items.push_back({key.trimmed(), value.trimmed()});
endInsertRows();
}
void MappingListModel::updateEntry(int index, const QString &key, const QString &value)
{
if (index < 0 || index >= m_items.size()) {
return;
}
m_items[index] = {key.trimmed(), value.trimmed()};
emit dataChanged(this->index(index), this->index(index));
}
void MappingListModel::removeEntry(int index)
{
if (index < 0 || index >= m_items.size()) {
return;
}
beginRemoveRows(QModelIndex(), index, index);
m_items.removeAt(index);
endRemoveRows();
}
void MappingListModel::clear()
{
beginResetModel();
m_items.clear();
endResetModel();
}
+48
View File
@@ -0,0 +1,48 @@
#ifndef MAPPING_LIST_MODEL_H
#define MAPPING_LIST_MODEL_H
#include <QAbstractListModel>
#include <QByteArray>
#include <QHash>
#include <QMap>
#include <QModelIndex>
#include <QString>
#include <QVariant>
#include <QVector>
class MappingListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
KeyRole = Qt::UserRole + 1,
ValueRole
};
explicit MappingListModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QHash<int, QByteArray> roleNames() const override;
void setMap(const QMap<QString, QString> &map);
QMap<QString, QString> toMap() const;
Q_INVOKABLE void addEntry(const QString &key, const QString &value);
Q_INVOKABLE void updateEntry(int index, const QString &key, const QString &value);
Q_INVOKABLE void removeEntry(int index);
Q_INVOKABLE void clear();
private:
struct Item {
QString key;
QString value;
};
QVector<Item> m_items;
};
#endif
+59
View File
@@ -0,0 +1,59 @@
#include "RecordListModel.h"
RecordListModel::RecordListModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int RecordListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_items.size();
}
QVariant RecordListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size()) {
return {};
}
const Item &item = m_items.at(index.row());
switch (role) {
case TimestampRole: return item.timestamp;
case ClassIdRole: return item.classId;
case ClassNameRole: return item.className;
case StudentIdRole: return item.studentId;
case NameRole: return item.name;
case ConfidenceRole: return item.confidence;
case ConfidenceTextRole: return QString::number(item.confidence * 100.0, 'f', 2) + "%";
case StatusRole: return item.status;
case StatusTextRole: return item.success ? QStringLiteral("成功") : QStringLiteral("失败");
case SuccessRole: return item.success;
default: return {};
}
}
QHash<int, QByteArray> RecordListModel::roleNames() const
{
return {
{TimestampRole, "timestamp"},
{ClassIdRole, "classId"},
{ClassNameRole, "className"},
{StudentIdRole, "studentId"},
{NameRole, "name"},
{ConfidenceRole, "confidence"},
{ConfidenceTextRole, "confidenceText"},
{StatusRole, "status"},
{StatusTextRole, "statusText"},
{SuccessRole, "success"}
};
}
void RecordListModel::setItems(const QVector<Item> &items)
{
beginResetModel();
m_items = items;
endResetModel();
}
+53
View File
@@ -0,0 +1,53 @@
#ifndef RECORD_LIST_MODEL_H
#define RECORD_LIST_MODEL_H
#include <QAbstractListModel>
#include <QByteArray>
#include <QHash>
#include <QModelIndex>
#include <QString>
#include <QVariant>
#include <QVector>
class RecordListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
TimestampRole = Qt::UserRole + 1,
ClassIdRole,
ClassNameRole,
StudentIdRole,
NameRole,
ConfidenceRole,
ConfidenceTextRole,
StatusRole,
StatusTextRole,
SuccessRole
};
explicit RecordListModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
struct Item {
QString timestamp;
QString classId;
QString className;
QString studentId;
QString name;
double confidence = 0.0;
QString status;
bool success = false;
};
void setItems(const QVector<Item> &items);
private:
QVector<Item> m_items;
};
#endif
+53
View File
@@ -0,0 +1,53 @@
#include "StatisticsListModel.h"
StatisticsListModel::StatisticsListModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int StatisticsListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_items.size();
}
QVariant StatisticsListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size()) {
return {};
}
const Item &item = m_items.at(index.row());
switch (role) {
case StudentIdRole: return item.studentId;
case NameRole: return item.name;
case TotalCountRole: return item.totalCount;
case TodayCountRole: return item.todayCount;
case LastCheckInRole: return item.lastCheckIn;
case AverageConfidenceRole: return item.averageConfidence;
case AverageConfidenceTextRole: return QString::number(item.averageConfidence * 100.0, 'f', 2) + "%";
default: return {};
}
}
QHash<int, QByteArray> StatisticsListModel::roleNames() const
{
return {
{StudentIdRole, "studentId"},
{NameRole, "name"},
{TotalCountRole, "totalCount"},
{TodayCountRole, "todayCount"},
{LastCheckInRole, "lastCheckIn"},
{AverageConfidenceRole, "averageConfidence"},
{AverageConfidenceTextRole, "averageConfidenceText"}
};
}
void StatisticsListModel::setItems(const QVector<Item> &items)
{
beginResetModel();
m_items = items;
endResetModel();
}
+48
View File
@@ -0,0 +1,48 @@
#ifndef STATISTICS_LIST_MODEL_H
#define STATISTICS_LIST_MODEL_H
#include <QAbstractListModel>
#include <QByteArray>
#include <QHash>
#include <QModelIndex>
#include <QString>
#include <QVariant>
#include <QVector>
class StatisticsListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
StudentIdRole = Qt::UserRole + 1,
NameRole,
TotalCountRole,
TodayCountRole,
LastCheckInRole,
AverageConfidenceRole,
AverageConfidenceTextRole
};
explicit StatisticsListModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
struct Item {
QString studentId;
QString name;
int totalCount = 0;
int todayCount = 0;
QString lastCheckIn;
double averageConfidence = 0.0;
};
void setItems(const QVector<Item> &items);
private:
QVector<Item> m_items;
};
#endif
+116
View File
@@ -0,0 +1,116 @@
#include "StudentProfileListModel.h"
StudentProfileListModel::StudentProfileListModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int StudentProfileListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_items.size();
}
QVariant StudentProfileListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size()) {
return {};
}
const Item &item = m_items.at(index.row());
switch (role) {
case StudentIdRole: return item.studentId;
case StudentNameRole: return item.studentName;
case ClassIdRole: return item.classId;
case ClassNameRole: return item.className;
case FaceIdsRole: return item.faceIds;
case BindingStatusRole: return item.bindingStatus;
default: return {};
}
}
bool StudentProfileListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size()) {
return false;
}
Item &item = m_items[index.row()];
switch (role) {
case StudentIdRole: item.studentId = value.toString().trimmed(); break;
case StudentNameRole: item.studentName = value.toString().trimmed(); break;
case ClassIdRole: item.classId = value.toString().trimmed(); break;
default: return false;
}
emit dataChanged(index, index, {role});
return true;
}
Qt::ItemFlags StudentProfileListModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}
QHash<int, QByteArray> StudentProfileListModel::roleNames() const
{
return {
{StudentIdRole, "studentId"},
{StudentNameRole, "studentName"},
{ClassIdRole, "classId"},
{ClassNameRole, "className"},
{FaceIdsRole, "faceIds"},
{BindingStatusRole, "bindingStatus"}
};
}
void StudentProfileListModel::setProfiles(const QVector<Item> &items)
{
beginResetModel();
m_items = items;
endResetModel();
}
QVector<StudentProfileListModel::Item> StudentProfileListModel::profiles() const
{
return m_items;
}
void StudentProfileListModel::addProfile(const QString &studentId, const QString &studentName, const QString &classId)
{
beginInsertRows(QModelIndex(), m_items.size(), m_items.size());
m_items.push_back({studentId.trimmed(), studentName.trimmed(), classId.trimmed(), {}, {}, {}});
endInsertRows();
}
void StudentProfileListModel::updateProfile(int index,
const QString &studentId,
const QString &studentName,
const QString &classId)
{
if (index < 0 || index >= m_items.size()) {
return;
}
Item &item = m_items[index];
item.studentId = studentId.trimmed();
item.studentName = studentName.trimmed();
item.classId = classId.trimmed();
emit dataChanged(this->index(index), this->index(index), {StudentIdRole, StudentNameRole, ClassIdRole});
}
void StudentProfileListModel::removeProfile(int index)
{
if (index < 0 || index >= m_items.size()) {
return;
}
beginRemoveRows(QModelIndex(), index, index);
m_items.removeAt(index);
endRemoveRows();
}
+54
View File
@@ -0,0 +1,54 @@
#ifndef STUDENT_PROFILE_LIST_MODEL_H
#define STUDENT_PROFILE_LIST_MODEL_H
#include <QAbstractListModel>
#include <QByteArray>
#include <QHash>
#include <QModelIndex>
#include <QString>
#include <QVariant>
#include <QVector>
class StudentProfileListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
StudentIdRole = Qt::UserRole + 1,
StudentNameRole,
ClassIdRole,
ClassNameRole,
FaceIdsRole,
BindingStatusRole
};
struct Item {
QString studentId;
QString studentName;
QString classId;
QString className;
QString faceIds;
QString bindingStatus;
};
explicit StudentProfileListModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QHash<int, QByteArray> roleNames() const override;
void setProfiles(const QVector<Item> &items);
QVector<Item> profiles() const;
void addProfile(const QString &studentId, const QString &studentName, const QString &classId);
void updateProfile(int index, const QString &studentId, const QString &studentName, const QString &classId);
void removeProfile(int index);
private:
QVector<Item> m_items;
};
#endif
+146
View File
@@ -0,0 +1,146 @@
#include "TimeSlotListModel.h"
#include <QJsonObject>
TimeSlotListModel::TimeSlotListModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int TimeSlotListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_timeSlots.size();
}
QVariant TimeSlotListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_timeSlots.size()) {
return {};
}
const TimeSlot &slot = m_timeSlots.at(index.row());
switch (role) {
case NameRole: return slot.name;
case StartTimeRole: return slot.startTime.toString("HH:mm");
case EndTimeRole: return slot.endTime.toString("HH:mm");
case EnabledRole: return slot.enabled;
default: return {};
}
}
bool TimeSlotListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_timeSlots.size()) {
return false;
}
TimeSlot &slot = m_timeSlots[index.row()];
switch (role) {
case NameRole: slot.name = value.toString(); break;
case StartTimeRole: slot.startTime = QTime::fromString(value.toString(), "HH:mm"); break;
case EndTimeRole: slot.endTime = QTime::fromString(value.toString(), "HH:mm"); break;
case EnabledRole: slot.enabled = value.toBool(); break;
default: return false;
}
emit dataChanged(index, index, {role});
return true;
}
Qt::ItemFlags TimeSlotListModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}
QHash<int, QByteArray> TimeSlotListModel::roleNames() const
{
return {
{NameRole, "name"},
{StartTimeRole, "startTime"},
{EndTimeRole, "endTime"},
{EnabledRole, "enabled"}
};
}
void TimeSlotListModel::setTimeSlots(const QVector<TimeSlot> &timeSlots)
{
beginResetModel();
m_timeSlots = timeSlots;
endResetModel();
}
QVector<TimeSlot> TimeSlotListModel::timeSlots() const
{
return m_timeSlots;
}
QJsonArray TimeSlotListModel::toJsonArray() const
{
QJsonArray array;
for (const TimeSlot &slot : m_timeSlots) {
QJsonObject object;
object["name"] = slot.name;
object["start_time"] = slot.startTime.toString("HH:mm");
object["end_time"] = slot.endTime.toString("HH:mm");
object["enabled"] = slot.enabled;
array.append(object);
}
return array;
}
QVariantMap TimeSlotListModel::get(int index) const
{
if (index < 0 || index >= m_timeSlots.size()) {
return {};
}
const TimeSlot &slot = m_timeSlots.at(index);
return {
{"name", slot.name},
{"startTime", slot.startTime.toString("HH:mm")},
{"endTime", slot.endTime.toString("HH:mm")},
{"enabled", slot.enabled}
};
}
void TimeSlotListModel::addTimeSlot(const QString &name, const QString &startTime, const QString &endTime, bool enabled)
{
beginInsertRows(QModelIndex(), m_timeSlots.size(), m_timeSlots.size());
m_timeSlots.push_back({name.trimmed(), QTime::fromString(startTime, "HH:mm"), QTime::fromString(endTime, "HH:mm"), enabled});
endInsertRows();
}
void TimeSlotListModel::updateTimeSlot(int index, const QString &name, const QString &startTime, const QString &endTime, bool enabled)
{
if (index < 0 || index >= m_timeSlots.size()) {
return;
}
TimeSlot &slot = m_timeSlots[index];
slot.name = name.trimmed();
slot.startTime = QTime::fromString(startTime, "HH:mm");
slot.endTime = QTime::fromString(endTime, "HH:mm");
slot.enabled = enabled;
emit dataChanged(this->index(index), this->index(index));
}
void TimeSlotListModel::removeTimeSlot(int index)
{
if (index < 0 || index >= m_timeSlots.size()) {
return;
}
beginRemoveRows(QModelIndex(), index, index);
m_timeSlots.removeAt(index);
endRemoveRows();
}
void TimeSlotListModel::resetToDefault()
{
setTimeSlots(defaultTimeSlots());
}
+48
View File
@@ -0,0 +1,48 @@
#ifndef TIMESLOT_LIST_MODEL_H
#define TIMESLOT_LIST_MODEL_H
#include <QAbstractListModel>
#include <QByteArray>
#include <QHash>
#include <QJsonArray>
#include <QModelIndex>
#include <QVariant>
#include <QVariantMap>
#include <QVector>
#include "AttendanceTypes.h"
class TimeSlotListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
NameRole = Qt::UserRole + 1,
StartTimeRole,
EndTimeRole,
EnabledRole
};
explicit TimeSlotListModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QHash<int, QByteArray> roleNames() const override;
void setTimeSlots(const QVector<TimeSlot> &timeSlots);
QVector<TimeSlot> timeSlots() const;
QJsonArray toJsonArray() const;
Q_INVOKABLE QVariantMap get(int index) const;
Q_INVOKABLE void addTimeSlot(const QString &name, const QString &startTime, const QString &endTime, bool enabled);
Q_INVOKABLE void updateTimeSlot(int index, const QString &name, const QString &startTime, const QString &endTime, bool enabled);
Q_INVOKABLE void removeTimeSlot(int index);
Q_INVOKABLE void resetToDefault();
private:
QVector<TimeSlot> m_timeSlots;
};
#endif
+217
View File
@@ -0,0 +1,217 @@
/*
* Tiny AES128 C++ 移植版本
* 与STM32端使用相同的AES实现,保证兼容性
*/
#ifndef _TINY_AES_HPP_
#define _TINY_AES_HPP_
#include <cstdint>
#include <cstring>
#define AES_BLOCKLEN 16
#define AES_KEYLEN 16
#define AES_keyExpSize 176
struct AES_ctx
{
uint8_t RoundKey[AES_keyExpSize];
uint8_t Iv[AES_BLOCKLEN];
};
static const uint8_t sbox[256] = {
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16
};
static const uint8_t rsbox[256] = {
0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d
};
static const uint8_t Rcon[11] = {
0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36
};
#define getSBoxValue(num) (sbox[(num)])
#define getSBoxInvert(num) (rsbox[(num)])
inline void KeyExpansion(uint8_t *RoundKey, const uint8_t *Key)
{
unsigned i, j, k;
uint8_t tempa[4];
for (i = 0; i < 4; ++i) {
RoundKey[(i * 4) + 0] = Key[(i * 4) + 0];
RoundKey[(i * 4) + 1] = Key[(i * 4) + 1];
RoundKey[(i * 4) + 2] = Key[(i * 4) + 2];
RoundKey[(i * 4) + 3] = Key[(i * 4) + 3];
}
for (i = 4; i < 44; ++i) {
k = (i - 1) * 4;
tempa[0] = RoundKey[k + 0];
tempa[1] = RoundKey[k + 1];
tempa[2] = RoundKey[k + 2];
tempa[3] = RoundKey[k + 3];
if (i % 4 == 0) {
const uint8_t u8tmp = tempa[0];
tempa[0] = tempa[1];
tempa[1] = tempa[2];
tempa[2] = tempa[3];
tempa[3] = u8tmp;
tempa[0] = getSBoxValue(tempa[0]);
tempa[1] = getSBoxValue(tempa[1]);
tempa[2] = getSBoxValue(tempa[2]);
tempa[3] = getSBoxValue(tempa[3]);
tempa[0] ^= Rcon[i / 4];
}
j = i * 4;
k = (i - 4) * 4;
RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0];
RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1];
RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2];
RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3];
}
}
static void AddRoundKey(uint8_t round, uint8_t *state, const uint8_t *RoundKey)
{
uint8_t i, j;
for (i = 0; i < 4; ++i) {
for (j = 0; j < 4; ++j) {
state[i * 4 + j] ^= RoundKey[(round * 16) + (i * 4 + j)];
}
}
}
static void InvSubBytes(uint8_t *state)
{
for (uint8_t i = 0; i < 16; ++i) {
state[i] = getSBoxInvert(state[i]);
}
}
static void InvShiftRows(uint8_t *state)
{
uint8_t temp;
temp = state[13];
state[13] = state[9];
state[9] = state[5];
state[5] = state[1];
state[1] = temp;
temp = state[2];
state[2] = state[10];
state[10] = temp;
temp = state[6];
state[6] = state[14];
state[14] = temp;
temp = state[3];
state[3] = state[7];
state[7] = state[11];
state[11] = state[15];
state[15] = temp;
}
static uint8_t xtime(uint8_t x)
{
return ((x << 1) ^ (((x >> 7) & 1U) * 0x1bU));
}
static uint8_t Multiply(uint8_t x, uint8_t y)
{
return (((y & 1U) * x) ^
(((y >> 1U) & 1U) * xtime(x)) ^
(((y >> 2U) & 1U) * xtime(xtime(x))) ^
(((y >> 3U) & 1U) * xtime(xtime(xtime(x)))) ^
(((y >> 4U) & 1U) * xtime(xtime(xtime(xtime(x))))));
}
static void InvMixColumns(uint8_t *state)
{
int i;
uint8_t a, b, c, d;
for (i = 0; i < 4; ++i) {
a = state[i * 4 + 0];
b = state[i * 4 + 1];
c = state[i * 4 + 2];
d = state[i * 4 + 3];
state[i * 4 + 0] = Multiply(a, 0x0eU) ^ Multiply(b, 0x0bU) ^ Multiply(c, 0x0dU) ^ Multiply(d, 0x09U);
state[i * 4 + 1] = Multiply(a, 0x09U) ^ Multiply(b, 0x0eU) ^ Multiply(c, 0x0bU) ^ Multiply(d, 0x0dU);
state[i * 4 + 2] = Multiply(a, 0x0dU) ^ Multiply(b, 0x09U) ^ Multiply(c, 0x0eU) ^ Multiply(d, 0x0bU);
state[i * 4 + 3] = Multiply(a, 0x0bU) ^ Multiply(b, 0x0dU) ^ Multiply(c, 0x09U) ^ Multiply(d, 0x0eU);
}
}
inline void InvCipher(uint8_t *state, const uint8_t *RoundKey)
{
uint8_t round = 0;
AddRoundKey(10, state, RoundKey);
for (round = 9; ; --round) {
InvShiftRows(state);
InvSubBytes(state);
AddRoundKey(round, state, RoundKey);
if (round == 0) {
break;
}
InvMixColumns(state);
}
}
inline void AES_init_ctx_iv(struct AES_ctx *ctx, const uint8_t *key, const uint8_t *iv)
{
KeyExpansion(ctx->RoundKey, key);
memcpy(ctx->Iv, iv, AES_BLOCKLEN);
}
inline void AES_CBC_decrypt_buffer(struct AES_ctx *ctx, uint8_t *buf, size_t length)
{
size_t i;
uint8_t storeNextIv[AES_BLOCKLEN];
for (i = 0; i < length; i += AES_BLOCKLEN) {
memcpy(storeNextIv, buf + i, AES_BLOCKLEN);
InvCipher(buf + i, ctx->RoundKey);
for (uint8_t j = 0; j < AES_BLOCKLEN; ++j) {
buf[i + j] ^= ctx->Iv[j];
}
memcpy(ctx->Iv, storeNextIv, AES_BLOCKLEN);
}
}
#endif