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
+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