This commit is contained in:
2026-05-05 00:16:23 +08:00
parent fca1ea21d2
commit b0854509fe
9 changed files with 936 additions and 649 deletions
+228 -147
View File
@@ -77,7 +77,8 @@ QNetworkRequest ApiClient::createRequest(const QString &url, bool encryptedRespo
request.setRawHeader("X-Api-Token", m_apiToken.toUtf8());
request.setRawHeader("Accept", "application/json");
if (jsonBody) {
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream");
request.setRawHeader("X-Encrypted", "AES128-CBC");
}
request.setTransferTimeout(NetworkTimeoutMs);
return request;
@@ -91,9 +92,9 @@ void ApiClient::fetchSyncState(qulonglong lastVersion)
});
qInfo() << "[ApiClient] GET sync state" << url;
QNetworkReply *reply = m_networkManager.get(createRequest(url));
QNetworkReply *reply = m_networkManager.get(createRequest(url, true));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
if (reply->error() != QNetworkReply::NoError) {
emitNetworkError(QStringLiteral("同步状态加载失败"), reply);
reply->deleteLater();
@@ -137,36 +138,70 @@ void ApiClient::fetchSyncState(qulonglong lastVersion)
emit timeSlotsFetched(timeSlots);
}
if (state.contains("mapping_config")) {
const QJsonObject mappingConfig = state.value("mapping_config").toObject();
if (state.contains("students") || state.contains("classes")) {
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 QJsonArray studentArray = state.value("students").toArray();
for (const QJsonValue &value : studentArray) {
const QJsonObject object = value.toObject();
const QString studentId = object.value("student_id").toString().trimmed();
if (studentId.isEmpty()) {
continue;
}
studentMap[studentId] = object.value("student_name").toString().trimmed();
studentClassMap[studentId] = object.value("class_id").toString().trimmed();
}
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 QJsonArray classes = state.value("classes").toArray();
for (const QJsonValue &value : classes) {
const QJsonObject object = value.toObject();
const QString classId = object.value("class_id").toString().trimmed();
if (!classId.isEmpty()) {
classMap[classId] = object.value("class_name").toString().trimmed();
}
}
const QJsonObject classObject = mappingConfig.value("class_name_map").toObject();
for (auto it = classObject.begin(); it != classObject.end(); ++it) {
classMap[it.key()] = it.value().toString();
QVector<StudentProfileListModel::Item> studentItems;
studentItems.reserve(studentMap.size());
for (auto it = studentMap.cbegin(); it != studentMap.cend(); ++it) {
const QString studentId = it.key();
studentItems.push_back({
studentId,
it.value(),
studentClassMap.value(studentId),
classMap.value(studentClassMap.value(studentId), studentClassMap.value(studentId)),
{},
{}
});
}
emit mappingConfigFetched(studentMap, studentClassMap, classMap);
emit studentsFetched(studentItems);
emit classesFetched(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();
if (state.contains("face_profiles")) {
const QJsonArray profiles = state.value("face_profiles").toArray();
QVector<FaceListModel::Item> faces;
faces.reserve(profiles.size());
for (const QJsonValue &value : profiles) {
const QJsonObject object = value.toObject();
const QString faceId = object.value("face_id").toString().trimmed();
const QString studentId = object.value("student_id").toString().trimmed();
if (faceId.isEmpty()) {
continue;
}
faces.push_back({
faceId,
studentId,
object.value("has_photo").toBool(false),
{},
{},
{},
{},
object.value("status").toString().trimmed()
});
}
emit faceIdMappingFetched(mapping);
emit faceProfilesFetched(faces);
}
if (state.contains("face_photos")) {
@@ -178,15 +213,6 @@ void ApiClient::fetchSyncState(qulonglong lastVersion)
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();
});
@@ -204,6 +230,18 @@ QByteArray ApiClient::maybeDecrypt(const QByteArray &data, bool encrypted) const
return CryptoUtils::decryptAes128CbcBase64(trimmed);
}
QByteArray ApiClient::decryptedReplyBody(QNetworkReply *reply) const
{
const QByteArray data = reply->readAll();
const bool encrypted = reply->rawHeader("X-Encrypted").trimmed() == "AES128-CBC";
return maybeDecrypt(data, encrypted);
}
QByteArray ApiClient::encryptedJsonBody(const QByteArray &body) const
{
return CryptoUtils::encryptAes128CbcBase64(body);
}
bool ApiClient::isConnectionError(QNetworkReply::NetworkError error) const
{
switch (error) {
@@ -280,21 +318,20 @@ void ApiClient::fetchRecords(int limit, int offset)
{"offset", QString::number(offset)}
});
qInfo() << "[ApiClient] GET" << url;
QNetworkReply *reply = m_networkManager.get(createRequest(url));
QNetworkReply *reply = m_networkManager.get(createRequest(url, true));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray rawData = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qInfo() << "[ApiClient] records status=" << statusCode
<< "error=" << reply->error()
<< "bytes=" << rawData.size()
<< "body=" << rawData.left(200);
<< "bytes=" << data.size()
<< "body=" << data.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();
@@ -320,21 +357,20 @@ void ApiClient::fetchStatistics()
{
const QString url = buildUrl("/v1/attendance/statistics");
qInfo() << "[ApiClient] GET" << url;
QNetworkReply *reply = m_networkManager.get(createRequest(url));
QNetworkReply *reply = m_networkManager.get(createRequest(url, true));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray rawData = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qInfo() << "[ApiClient] statistics status=" << statusCode
<< "error=" << reply->error()
<< "bytes=" << rawData.size()
<< "body=" << rawData.left(200);
<< "bytes=" << data.size()
<< "body=" << data.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();
@@ -352,9 +388,10 @@ void ApiClient::fetchStatistics()
void ApiClient::clearAllRecords()
{
QNetworkReply *reply = m_networkManager.post(createRequest(buildUrl("/v1/attendance/records/clear"), false, true), QByteArray());
QNetworkReply *reply = m_networkManager.post(createRequest(buildUrl("/v1/attendance/records/clear"), true, true),
encryptedJsonBody("{}"));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
if (reply->error() != QNetworkReply::NoError) {
if (isConnectionError(reply->error())) {
emit connectionUnavailable(QStringLiteral("清空失败: %1").arg(reply->errorString()));
@@ -375,9 +412,9 @@ void ApiClient::deleteRecord(const QString &timestamp, const QString &studentId)
QNetworkReply *reply = m_networkManager.deleteResource(createRequest(buildUrl("/v1/attendance/records", {
{"timestamp", timestamp},
{"student_id", studentId}
})));
}), true));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
if (reply->error() != QNetworkReply::NoError) {
if (isConnectionError(reply->error())) {
emit connectionUnavailable(QStringLiteral("记录删除失败: %1").arg(reply->errorString()));
@@ -395,9 +432,9 @@ void ApiClient::deleteRecord(const QString &timestamp, const QString &studentId)
void ApiClient::fetchTimeSlots()
{
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/config/time-slots")));
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/config/time-slots"), true));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
if (reply->error() != QNetworkReply::NoError) {
emitNetworkError(QStringLiteral("加载时间段失败"), reply);
reply->deleteLater();
@@ -439,9 +476,10 @@ void ApiClient::uploadTimeSlots(const QVector<TimeSlot> &timeSlots)
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);
QNetworkReply *reply = m_networkManager.post(createRequest(buildUrl("/v1/config/time-slots"), true, true),
encryptedJsonBody(body));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
if (reply->error() != QNetworkReply::NoError) {
if (isConnectionError(reply->error())) {
emit connectionUnavailable(QStringLiteral("时间段同步失败: %1").arg(reply->errorString()));
@@ -456,139 +494,203 @@ void ApiClient::uploadTimeSlots(const QVector<TimeSlot> &timeSlots)
});
}
void ApiClient::fetchMappingConfig()
void ApiClient::fetchStudents()
{
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/config/mappings")));
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/students"), true));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
if (reply->error() != QNetworkReply::NoError) {
emitNetworkError(QStringLiteral("加载映射配置失败"), reply);
emitNetworkError(QStringLiteral("加载学生档案失败"), reply);
reply->deleteLater();
return;
}
QVector<StudentProfileListModel::Item> students;
const QJsonArray array = QJsonDocument::fromJson(data).object().value("students").toArray();
students.reserve(array.size());
for (const QJsonValue &value : array) {
const QJsonObject object = value.toObject();
students.push_back({
object.value("student_id").toString().trimmed(),
object.value("student_name").toString().trimmed(),
object.value("class_id").toString().trimmed(),
{},
{},
{}
});
}
emit studentsFetched(students);
reply->deleteLater();
});
}
void ApiClient::uploadStudents(const QVector<StudentProfileListModel::Item> &students)
{
QJsonArray array;
for (const StudentProfileListModel::Item &student : students) {
const QString studentId = student.studentId.trimmed();
if (studentId.isEmpty()) {
continue;
}
QJsonObject object;
object["student_id"] = studentId;
object["student_name"] = student.studentName.trimmed();
object["class_id"] = student.classId.trimmed();
array.append(object);
}
QJsonObject payload;
payload["students"] = array;
const QByteArray body = QJsonDocument(payload).toJson(QJsonDocument::Compact);
QNetworkReply *reply = m_networkManager.post(createRequest(buildUrl("/v1/students"), true, true),
encryptedJsonBody(body));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = decryptedReplyBody(reply);
if (reply->error() != QNetworkReply::NoError) {
if (isConnectionError(reply->error())) {
emit connectionUnavailable(QStringLiteral("学生档案保存失败: %1").arg(reply->errorString()));
}
emit studentsUploaded(false, reply->errorString());
reply->deleteLater();
return;
}
const QJsonObject object = QJsonDocument::fromJson(data).object();
emit studentsUploaded(object.value("success").toBool(), object.value("message").toString());
reply->deleteLater();
});
}
void ApiClient::fetchClasses()
{
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/classes"), true));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = decryptedReplyBody(reply);
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 QJsonArray array = QJsonDocument::fromJson(data).object().value("classes").toArray();
for (const QJsonValue &value : array) {
const QJsonObject object = value.toObject();
const QString classId = object.value("class_id").toString().trimmed();
if (!classId.isEmpty()) {
classMap[classId] = object.value("class_name").toString().trimmed();
}
}
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);
emit classesFetched(classMap);
reply->deleteLater();
});
}
void ApiClient::uploadMappingConfig(const QMap<QString, QString> &studentNameMap,
const QMap<QString, QString> &studentClassMap,
const QMap<QString, QString> &classNameMap)
void ApiClient::uploadClasses(const QMap<QString, QString> &classNameMap)
{
QJsonObject studentObject;
for (auto it = studentNameMap.begin(); it != studentNameMap.end(); ++it) {
studentObject[it.key()] = it.value();
QJsonArray array;
for (auto it = classNameMap.cbegin(); it != classNameMap.cend(); ++it) {
const QString classId = it.key().trimmed();
if (classId.isEmpty()) {
continue;
}
QJsonObject object;
object["class_id"] = classId;
object["class_name"] = it.value().trimmed();
array.append(object);
}
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));
payload["classes"] = array;
const QByteArray body = QJsonDocument(payload).toJson(QJsonDocument::Compact);
QNetworkReply *reply = m_networkManager.post(createRequest(buildUrl("/v1/classes"), true, true),
encryptedJsonBody(body));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
if (reply->error() != QNetworkReply::NoError) {
if (isConnectionError(reply->error())) {
emit connectionUnavailable(QStringLiteral("映射保存失败: %1").arg(reply->errorString()));
emit connectionUnavailable(QStringLiteral("班级配置保存失败: %1").arg(reply->errorString()));
}
emit mappingConfigUploaded(false, reply->errorString());
emit classesUploaded(false, reply->errorString());
reply->deleteLater();
return;
}
const QJsonObject object = QJsonDocument::fromJson(data).object();
emit mappingConfigUploaded(object.value("success").toBool(), object.value("message").toString());
emit classesUploaded(object.value("success").toBool(), object.value("message").toString());
reply->deleteLater();
});
}
void ApiClient::fetchFaceIdMapping()
void ApiClient::fetchFaceProfiles()
{
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/faces/mappings")));
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/face_profiles"), true));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
if (reply->error() != QNetworkReply::NoError) {
emitNetworkError(QStringLiteral("加载人脸映射失败"), reply);
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();
QVector<FaceListModel::Item> faces;
const QJsonArray array = QJsonDocument::fromJson(data).object().value("face_profiles").toArray();
faces.reserve(array.size());
for (const QJsonValue &value : array) {
const QJsonObject object = value.toObject();
faces.push_back({
object.value("face_id").toString().trimmed(),
object.value("student_id").toString().trimmed(),
object.value("has_photo").toBool(false),
{},
{},
{},
{},
object.value("status").toString().trimmed()
});
}
emit faceIdMappingFetched(mapping);
emit faceProfilesFetched(faces);
reply->deleteLater();
});
}
void ApiClient::uploadFaceIdMapping(const QMap<QString, QString> &mapping)
void ApiClient::uploadFaceProfiles(const QVector<FaceListModel::Item> &faces)
{
QJsonObject mappings;
for (auto it = mapping.begin(); it != mapping.end(); ++it) {
if (!it.value().trimmed().isEmpty()) {
mappings[it.key()] = it.value();
QJsonArray array;
for (const FaceListModel::Item &face : faces) {
const QString faceId = face.faceId.trimmed();
if (faceId.isEmpty()) {
continue;
}
QJsonObject object;
object["face_id"] = faceId;
object["student_id"] = face.studentId.trimmed();
object["status"] = face.studentId.trimmed().isEmpty() ? QStringLiteral("unbound") : QStringLiteral("active");
array.append(object);
}
QJsonObject payload;
payload["mappings"] = mappings;
QNetworkReply *reply = m_networkManager.post(createRequest(buildUrl("/v1/faces/mappings"), false, true),
QJsonDocument(payload).toJson(QJsonDocument::Compact));
payload["face_profiles"] = array;
const QByteArray body = QJsonDocument(payload).toJson(QJsonDocument::Compact);
QNetworkReply *reply = m_networkManager.post(createRequest(buildUrl("/v1/face_profiles"), true, true),
encryptedJsonBody(body));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
if (reply->error() != QNetworkReply::NoError) {
if (isConnectionError(reply->error())) {
emit connectionUnavailable(QStringLiteral("人脸映射保存失败: %1").arg(reply->errorString()));
emit connectionUnavailable(QStringLiteral("人脸档案保存失败: %1").arg(reply->errorString()));
}
emit faceIdMappingUploaded(false, reply->errorString());
emit faceProfilesUploaded(false, reply->errorString());
reply->deleteLater();
return;
}
const QJsonObject object = QJsonDocument::fromJson(data).object();
emit faceIdMappingUploaded(object.value("success").toBool(), object.value("message").toString());
emit faceProfilesUploaded(object.value("success").toBool(), object.value("message").toString());
reply->deleteLater();
});
}
void ApiClient::fetchFacePhotos()
{
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/faces/photos")));
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/faces/photos"), true));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
if (reply->error() != QNetworkReply::NoError) {
emitNetworkError(QStringLiteral("加载照片列表失败"), reply);
reply->deleteLater();
@@ -634,27 +736,6 @@ void ApiClient::fetchFacePhoto(const QString &faceId)
});
}
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();
@@ -665,9 +746,9 @@ void ApiClient::deleteFace(const QString &studentId, bool deleteRecords)
if (deleteRecords) {
url += "?delete_records=true";
}
QNetworkReply *reply = m_networkManager.deleteResource(createRequest(url));
QNetworkReply *reply = m_networkManager.deleteResource(createRequest(url, true));
connect(reply, &QNetworkReply::finished, this, [this, reply, studentId]() {
const QByteArray data = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
if (reply->error() != QNetworkReply::NoError) {
if (isConnectionError(reply->error())) {
emit connectionUnavailable(QStringLiteral("删除失败: %1").arg(reply->errorString()));
@@ -688,9 +769,9 @@ void ApiClient::deleteFace(const QString &studentId, bool deleteRecords)
void ApiClient::checkHealth()
{
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/system/health")));
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/system/health"), true));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QByteArray data = reply->readAll();
const QByteArray data = decryptedReplyBody(reply);
if (reply->error() != QNetworkReply::NoError) {
emit healthChecked(false, reply->errorString());
reply->deleteLater();
+16 -14
View File
@@ -10,6 +10,8 @@
#include <QStringList>
#include <QVector>
#include "AttendanceTypes.h"
#include "StudentProfileListModel.h"
#include "FaceListModel.h"
class ApiClient : public QObject
{
@@ -30,15 +32,14 @@ public:
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 fetchStudents();
void uploadStudents(const QVector<StudentProfileListModel::Item> &students);
void fetchClasses();
void uploadClasses(const QMap<QString, QString> &classNameMap);
void fetchFaceProfiles();
void uploadFaceProfiles(const QVector<FaceListModel::Item> &faces);
void fetchFacePhotos();
void fetchFacePhoto(const QString &faceId);
void fetchAllStudentIds();
void deleteFace(const QString &studentId, bool deleteRecords);
void checkHealth();
@@ -49,15 +50,14 @@ signals:
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 studentsFetched(const QVector<StudentProfileListModel::Item> &students);
void studentsUploaded(bool success, const QString &message);
void classesFetched(const QMap<QString, QString> &classNameMap);
void classesUploaded(bool success, const QString &message);
void faceProfilesFetched(const QVector<FaceListModel::Item> &faces);
void faceProfilesUploaded(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);
@@ -69,6 +69,8 @@ private:
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;
QByteArray decryptedReplyBody(QNetworkReply *reply) const;
QByteArray encryptedJsonBody(const QByteArray &body) const;
void emitNetworkError(const QString &prefix, QNetworkReply *reply);
bool isConnectionError(QNetworkReply::NetworkError error) const;
+251 -85
View File
@@ -198,33 +198,67 @@ AppController::AppController(QObject *parent)
setStatusMessage(success ? QStringLiteral("时间段已保存并同步") : QStringLiteral("时间段同步失败: %1").arg(message));
});
connect(&m_apiClient, &ApiClient::mappingConfigFetched, this,
[this](const QMap<QString, QString> &studentNameMap,
const QMap<QString, QString> &studentClassMap,
const QMap<QString, QString> &classNameMap) {
m_studentMappingModel.setMap(studentNameMap);
m_studentClassMappingModel.setMap(studentClassMap);
m_classMappingModel.setMap(classNameMap);
rebuildStudentProfiles();
connect(&m_apiClient, &ApiClient::studentsFetched, this, [this](const QVector<StudentProfileListModel::Item> &students) {
applyFetchedStudents(students);
saveMappingsToLocal();
updateFilterOptions();
rebuildFilteredRecords();
rebuildStatistics();
setStatusMessage("映射配置已同步");
setStatusMessage(QStringLiteral("学生档案已同步"));
});
connect(&m_apiClient, &ApiClient::mappingConfigUploaded, this, [this](bool success, const QString &message) {
setStatusMessage(success ? QStringLiteral("映射配置已保存") : QStringLiteral("映射保存失败: %1").arg(message));
connect(&m_apiClient, &ApiClient::studentsUploaded, this, [this](bool success, const QString &message) {
if (m_mappingSaveInProgress) {
if (success) {
m_apiClient.uploadClasses(m_pendingClassMap);
setStatusMessage(QStringLiteral("学生档案已保存,正在保存班级"));
} else {
m_mappingSaveInProgress = false;
m_pendingClassMap.clear();
m_pendingFaceItems.clear();
setStatusMessage(QStringLiteral("学生档案保存失败: %1").arg(message));
}
return;
}
setStatusMessage(success ? QStringLiteral("学生档案已保存") : QStringLiteral("学生档案保存失败: %1").arg(message));
});
connect(&m_apiClient, &ApiClient::faceIdMappingFetched, this, [this](const QMap<QString, QString> &mapping) {
m_faceIdMapping = mapping;
mergeFaceItems();
rebuildStudentProfiles();
connect(&m_apiClient, &ApiClient::classesFetched, this, [this](const QMap<QString, QString> &classNameMap) {
applyFetchedClasses(classNameMap);
saveMappingsToLocal();
updateFilterOptions();
rebuildFilteredRecords();
rebuildStatistics();
setStatusMessage(QStringLiteral("班级配置已同步"));
});
connect(&m_apiClient, &ApiClient::faceIdMappingUploaded, this, [this](bool success, const QString &message) {
setStatusMessage(success ? QStringLiteral("人脸映射已保存") : QStringLiteral("人脸映射保存失败: %1").arg(message));
connect(&m_apiClient, &ApiClient::classesUploaded, this, [this](bool success, const QString &message) {
if (m_mappingSaveInProgress) {
if (success) {
m_apiClient.uploadFaceProfiles(m_pendingFaceItems);
setStatusMessage(QStringLiteral("班级配置已保存,正在保存人脸档案"));
} else {
m_mappingSaveInProgress = false;
m_pendingClassMap.clear();
m_pendingFaceItems.clear();
setStatusMessage(QStringLiteral("班级保存失败: %1").arg(message));
}
return;
}
setStatusMessage(success ? QStringLiteral("班级配置已保存") : QStringLiteral("班级保存失败: %1").arg(message));
});
connect(&m_apiClient, &ApiClient::faceProfilesFetched, this, [this](const QVector<FaceListModel::Item> &faces) {
applyFetchedFaceProfiles(faces);
});
connect(&m_apiClient, &ApiClient::faceProfilesUploaded, this, [this](bool success, const QString &message) {
if (m_mappingSaveInProgress) {
m_mappingSaveInProgress = false;
m_pendingClassMap.clear();
m_pendingFaceItems.clear();
}
setStatusMessage(success ? QStringLiteral("人脸档案已保存") : QStringLiteral("人脸档案保存失败: %1").arg(message));
});
connect(&m_apiClient, &ApiClient::facePhotosFetched, this, [this](const QStringList &photoIds) {
@@ -451,7 +485,7 @@ void AppController::setStudentProfileBindingFilter(const QString &bindingFilter)
void AppController::refreshAll()
{
m_apiClient.fetchSyncState(m_qtSyncVersion);
m_apiClient.fetchSyncState(0);
}
void AppController::refreshRecords()
@@ -505,7 +539,7 @@ bool AppController::exportFilteredRecords(const QString &fileUrl)
out << csvCell(record.timestamp) << ","
<< csvCell(effectiveClassId(record)) << ","
<< csvCell(mappedClassName(record)) << ","
<< csvCell(record.studentId) << ","
<< csvCell(effectiveStudentId(record)) << ","
<< csvCell(mappedStudentName(record).isEmpty() ? record.originalName : mappedStudentName(record)) << ","
<< QString::number(record.confidence, 'f', 4) << ","
<< csvCell(record.status) << "\n";
@@ -566,7 +600,9 @@ void AppController::reloadTimeSlotsFromServer()
void AppController::loadMappingsFromServer()
{
m_apiClient.fetchMappingConfig();
m_apiClient.fetchStudents();
m_apiClient.fetchClasses();
m_apiClient.fetchFaceProfiles();
}
void AppController::saveMappings()
@@ -586,10 +622,10 @@ void AppController::saveMappings()
}
saveMappingsToLocal();
m_apiClient.uploadMappingConfig(m_studentMappingModel.toMap(),
m_studentClassMappingModel.toMap(),
m_classMappingModel.toMap());
m_apiClient.uploadFaceIdMapping(m_faceIdMapping);
m_mappingSaveInProgress = true;
m_pendingClassMap = m_classMappingModel.toMap();
m_pendingFaceItems = m_faceModel.items();
m_apiClient.uploadStudents(m_studentProfileModel.profiles());
rebuildStudentProfiles();
mergeFaceItems();
updateFilterOptions();
@@ -630,6 +666,23 @@ void AppController::updateStudentProfile(int index,
emit studentProfilesChanged();
}
QString AppController::studentIdChangeImpactMessage(int index, const QString &newStudentId) const
{
const QVector<StudentProfileListModel::Item> profiles = m_studentProfileModel.profiles();
if (index < 0 || index >= profiles.size()) {
return {};
}
const QString oldStudentId = profiles.at(index).studentId.trimmed();
const QString trimmedNewId = newStudentId.trimmed();
if (oldStudentId.isEmpty() || trimmedNewId.isEmpty() || oldStudentId == trimmedNewId) {
return {};
}
return QStringLiteral("修改学号会同步影响该学生的人脸绑定、考勤记录显示、班级筛选和导出结果。确定将 %1 改为 %2 吗?")
.arg(oldStudentId, trimmedNewId);
}
void AppController::removeStudentProfile(int index)
{
const QVector<StudentProfileListModel::Item> profiles = m_studentProfileModel.profiles();
@@ -653,6 +706,32 @@ void AppController::removeStudentProfile(int index)
emit studentProfilesChanged();
}
bool AppController::removeClassMapping(int index)
{
const QModelIndex modelIndex = m_classMappingModel.index(index);
if (!modelIndex.isValid()) {
return false;
}
const QString classId = m_classMappingModel.data(modelIndex, MappingListModel::KeyRole).toString().trimmed();
const QString normalizedClassId = classId.isEmpty() ? QStringLiteral("0") : classId;
int studentCount = 0;
const QVector<StudentProfileListModel::Item> profiles = m_studentProfileModel.profiles();
for (const StudentProfileListModel::Item &profile : profiles) {
if (profile.classId.trimmed() == normalizedClassId) {
studentCount++;
}
}
if (studentCount > 0) {
setStatusMessage(QStringLiteral("班级 %1 下还有 %2 名学生,不能删除").arg(normalizedClassId).arg(studentCount));
return false;
}
m_classMappingModel.removeEntry(index);
return true;
}
QString AppController::classDisplayForId(const QString &classId) const
{
const QString trimmed = classId.trimmed();
@@ -671,10 +750,73 @@ QString AppController::classIdFromDisplay(const QString &display) const
void AppController::refreshFaces()
{
m_apiClient.fetchFaceIdMapping();
m_apiClient.fetchFaceProfiles();
m_apiClient.fetchFacePhotos();
}
bool AppController::editFaceProfile(const QString &faceId,
const QString &studentId,
const QString &studentName,
const QString &classId)
{
const QString trimmedFaceId = faceId.trimmed();
const QString trimmedStudentId = studentId.trimmed();
const QString trimmedStudentName = studentName.trimmed();
const QString trimmedClassId = classId.trimmed();
if (trimmedFaceId.isEmpty()) {
setStatusMessage(QStringLiteral("人脸编号不能为空"));
return false;
}
if (!trimmedStudentId.isEmpty() && !trimmedClassId.isEmpty()) {
const QMap<QString, QString> classMap = m_classMappingModel.toMap();
if (!classMap.contains(trimmedClassId)) {
setStatusMessage(QStringLiteral("班级 %1 不存在,请先在配置中心新增班级").arg(trimmedClassId));
return false;
}
}
const QString oldStudentId = m_faceIdMapping.value(trimmedFaceId).trimmed();
if (trimmedStudentId.isEmpty()) {
m_faceIdMapping.remove(trimmedFaceId);
} else {
m_faceIdMapping[trimmedFaceId] = trimmedStudentId;
QVector<StudentProfileListModel::Item> profiles = m_studentProfileModel.profiles();
int existingIndex = -1;
int oldStudentIndex = -1;
for (int i = 0; i < profiles.size(); ++i) {
if (profiles.at(i).studentId.trimmed() == trimmedStudentId) {
existingIndex = i;
}
if (!oldStudentId.isEmpty() && profiles.at(i).studentId.trimmed() == oldStudentId) {
oldStudentIndex = i;
}
}
if (existingIndex >= 0) {
m_studentProfileModel.updateProfile(existingIndex, trimmedStudentId, trimmedStudentName, trimmedClassId);
} else if (oldStudentIndex >= 0) {
m_studentProfileModel.updateProfile(oldStudentIndex, trimmedStudentId, trimmedStudentName, trimmedClassId);
} else {
m_studentProfileModel.addProfile(trimmedStudentId, trimmedStudentName, trimmedClassId);
}
applyStudentProfilesToMappings();
}
mergeFaceItems();
rebuildStudentProfiles();
updateFilterOptions();
rebuildFilteredRecords();
rebuildStatistics();
emit studentProfilesChanged();
setStatusMessage(QStringLiteral("人脸档案已更新,正在保存到服务端"));
return true;
}
void AppController::updateFaceStudentId(int index, const QString &studentId)
{
const QVector<FaceListModel::Item> items = m_faceModel.items();
@@ -699,28 +841,7 @@ void AppController::updateFaceStudentId(int index, const QString &studentId)
void AppController::saveFaceMappings()
{
QString validationMessage;
if (!validateFaceMappings(&validationMessage)) {
setStatusMessage(validationMessage);
mergeFaceItems();
return;
}
QMap<QString, QString> mapping;
const QVector<FaceListModel::Item> items = m_faceModel.items();
for (const FaceListModel::Item &item : items) {
const QString faceId = item.faceId.trimmed();
const QString studentId = item.studentId.trimmed();
if (faceId.isEmpty() || studentId.isEmpty()) {
continue;
}
mapping[faceId] = studentId;
}
m_faceIdMapping = mapping;
mergeFaceItems();
rebuildStudentProfiles();
m_apiClient.uploadFaceIdMapping(mapping);
saveMappings();
}
void AppController::deleteFace(const QString &studentId, bool deleteRecords)
@@ -760,7 +881,7 @@ void AppController::saveApiToken()
void AppController::saveMappingsToLocal()
{
const QString path = localSettingsPath("mapping_config.json");
const QString path = localSettingsPath("profiles_cache.json");
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
return;
@@ -785,15 +906,15 @@ void AppController::saveMappingsToLocal()
studentClassObject[it.key()] = it.value();
}
root["student_name_map"] = studentObject;
root["student_class_map"] = studentClassObject;
root["class_name_map"] = classObject;
root["students"] = studentObject;
root["student_classes"] = studentClassObject;
root["classes"] = classObject;
file.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
}
void AppController::loadMappingsFromLocal()
{
QFile file(localSettingsPath("mapping_config.json"));
QFile file(localSettingsPath("profiles_cache.json"));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
m_studentMappingModel.clear();
m_studentClassMappingModel.clear();
@@ -803,19 +924,19 @@ void AppController::loadMappingsFromLocal()
const QJsonObject root = QJsonDocument::fromJson(file.readAll()).object();
QMap<QString, QString> studentMap;
const QJsonObject studentObject = root.value("student_name_map").toObject();
const QJsonObject studentObject = root.value("students").toObject();
for (auto it = studentObject.begin(); it != studentObject.end(); ++it) {
studentMap[it.key()] = it.value().toString();
}
QMap<QString, QString> classMap;
const QJsonObject classObject = root.value("class_name_map").toObject();
const QJsonObject classObject = root.value("classes").toObject();
for (auto it = classObject.begin(); it != classObject.end(); ++it) {
classMap[it.key()] = it.value().toString();
}
QMap<QString, QString> studentClassMap;
const QJsonObject studentClassObject = root.value("student_class_map").toObject();
const QJsonObject studentClassObject = root.value("student_classes").toObject();
for (auto it = studentClassObject.begin(); it != studentClassObject.end(); ++it) {
studentClassMap[it.key()] = it.value().toString();
}
@@ -899,7 +1020,7 @@ void AppController::updateFilterOptions()
for (const AttendanceRecord &record : m_allRecords) {
classIds.insert(effectiveClassId(record));
studentIds.insert(record.studentId);
studentIds.insert(effectiveStudentId(record));
}
m_classOptions = {"全部"};
@@ -915,6 +1036,63 @@ void AppController::updateFilterOptions()
emit filtersChanged();
}
void AppController::applyFetchedStudents(const QVector<StudentProfileListModel::Item> &students)
{
QMap<QString, QString> studentNameMap;
QMap<QString, QString> studentClassMap;
for (const StudentProfileListModel::Item &student : students) {
const QString studentId = student.studentId.trimmed();
if (studentId.isEmpty()) {
continue;
}
studentNameMap[studentId] = student.studentName.trimmed();
studentClassMap[studentId] = student.classId.trimmed();
}
m_studentMappingModel.setMap(studentNameMap);
m_studentClassMappingModel.setMap(studentClassMap);
m_studentsLoadedFromServer = true;
for (auto it = m_faceIdMapping.begin(); it != m_faceIdMapping.end();) {
if (!studentNameMap.contains(it.value().trimmed())) {
it = m_faceIdMapping.erase(it);
} else {
++it;
}
}
rebuildStudentProfiles();
mergeFaceItems();
}
void AppController::applyFetchedClasses(const QMap<QString, QString> &classNameMap)
{
m_classMappingModel.setMap(classNameMap);
rebuildStudentProfiles();
mergeFaceItems();
}
void AppController::applyFetchedFaceProfiles(const QVector<FaceListModel::Item> &faces)
{
QMap<QString, QString> mapping;
const QMap<QString, QString> studentNameMap = m_studentMappingModel.toMap();
QStringList faceIds;
for (const FaceListModel::Item &face : faces) {
const QString faceId = face.faceId.trimmed();
if (faceId.isEmpty()) {
continue;
}
if (!faceIds.contains(faceId)) {
faceIds.push_back(faceId);
}
const QString studentId = face.studentId.trimmed();
if (!studentId.isEmpty() && (!m_studentsLoadedFromServer || studentNameMap.contains(studentId))) {
mapping[faceId] = studentId;
}
}
m_faceIdMapping = mapping;
m_facePhotoIds = faceIds;
mergeFaceItems();
rebuildStudentProfiles();
}
void AppController::rebuildStudentProfiles()
{
const QMap<QString, QString> studentNameMap = m_studentMappingModel.toMap();
@@ -926,16 +1104,8 @@ void AppController::rebuildStudentProfiles()
studentIds.insert(it.key());
}
for (auto it = studentClassMap.cbegin(); it != studentClassMap.cend(); ++it) {
studentIds.insert(it.key());
}
for (auto it = m_faceIdMapping.cbegin(); it != m_faceIdMapping.cend(); ++it) {
if (!it.value().trimmed().isEmpty()) {
studentIds.insert(it.value().trimmed());
}
}
for (const AttendanceRecord &record : m_allRecords) {
if (!record.studentId.trimmed().isEmpty()) {
studentIds.insert(record.studentId.trimmed());
if (studentNameMap.contains(it.key())) {
studentIds.insert(it.key());
}
}
@@ -1021,19 +1191,7 @@ bool AppController::validateMappings(QString *message) const
}
return false;
}
if (studentName.isEmpty()) {
if (message) {
*message = QStringLiteral("学号 %1 的姓名不能为空").arg(studentId);
}
return false;
}
if (classId.isEmpty()) {
if (message) {
*message = QStringLiteral("学号 %1 的班级不能为空").arg(studentId);
}
return false;
}
if (!classMap.contains(classId)) {
if (!classId.isEmpty() && !classMap.contains(classId)) {
if (message) {
*message = QStringLiteral("学号 %1 选择的班级 %2 不存在").arg(studentId, classId);
}
@@ -1111,7 +1269,8 @@ void AppController::rebuildFilteredRecords()
continue;
}
if (m_selectedStudentId != "全部" && record.studentId != m_selectedStudentId) {
const QString studentId = effectiveStudentId(record);
if (m_selectedStudentId != "全部" && studentId != m_selectedStudentId) {
continue;
}
@@ -1148,7 +1307,7 @@ void AppController::rebuildFilteredRecords()
record.timestamp,
effectiveClassId(record),
mappedClassName(record),
record.studentId,
effectiveStudentId(record),
mappedStudentName(record).isEmpty() ? record.originalName : mappedStudentName(record),
record.confidence,
record.status,
@@ -1171,8 +1330,9 @@ void AppController::rebuildStatistics()
continue;
}
StudentStatistics &stats = statisticsMap[record.studentId];
stats.studentId = record.studentId;
const QString studentId = effectiveStudentId(record);
StudentStatistics &stats = statisticsMap[studentId];
stats.studentId = studentId;
stats.name = mappedStudentName(record).isEmpty() ? record.originalName : mappedStudentName(record);
stats.totalCount += 1;
stats.averageConfidence += record.confidence;
@@ -1215,12 +1375,18 @@ void AppController::rebuildStatistics()
QString AppController::mappedStudentName(const AttendanceRecord &record) const
{
return m_studentMappingModel.toMap().value(record.studentId);
return m_studentMappingModel.toMap().value(effectiveStudentId(record));
}
QString AppController::effectiveStudentId(const AttendanceRecord &record) const
{
const QString recordStudentId = record.studentId.trimmed();
return m_faceIdMapping.value(recordStudentId, recordStudentId).trimmed();
}
QString AppController::effectiveClassId(const AttendanceRecord &record) const
{
return m_studentClassMappingModel.toMap().value(record.studentId, record.classId);
return m_studentClassMappingModel.toMap().value(effectiveStudentId(record), record.classId);
}
QString AppController::mappedClassName(const AttendanceRecord &record) const
+14
View File
@@ -105,10 +105,16 @@ public:
const QString &studentId,
const QString &studentName,
const QString &classId);
Q_INVOKABLE QString studentIdChangeImpactMessage(int index, const QString &newStudentId) const;
Q_INVOKABLE void removeStudentProfile(int index);
Q_INVOKABLE bool removeClassMapping(int index);
Q_INVOKABLE QString classDisplayForId(const QString &classId) const;
Q_INVOKABLE QString classIdFromDisplay(const QString &display) const;
Q_INVOKABLE void refreshFaces();
Q_INVOKABLE bool editFaceProfile(const QString &faceId,
const QString &studentId,
const QString &studentName,
const QString &classId);
Q_INVOKABLE void updateFaceStudentId(int index, const QString &studentId);
Q_INVOKABLE void saveFaceMappings();
Q_INVOKABLE void deleteFace(const QString &studentId, bool deleteRecords);
@@ -136,6 +142,9 @@ private:
void loadTimeSlotsFromLocal();
QString localSettingsPath(const QString &fileName) const;
void updateFilterOptions();
void applyFetchedStudents(const QVector<StudentProfileListModel::Item> &students);
void applyFetchedClasses(const QMap<QString, QString> &classNameMap);
void applyFetchedFaceProfiles(const QVector<FaceListModel::Item> &faces);
void rebuildStudentProfiles();
void applyStudentProfilesToMappings();
bool validateMappings(QString *message) const;
@@ -143,6 +152,7 @@ private:
QString faceBindingTextForStudent(const QString &studentId) const;
void rebuildFilteredRecords();
void rebuildStatistics();
QString effectiveStudentId(const AttendanceRecord &record) const;
QString mappedStudentName(const AttendanceRecord &record) const;
QString effectiveClassId(const AttendanceRecord &record) const;
QString mappedClassName(const AttendanceRecord &record) const;
@@ -196,6 +206,10 @@ private:
QMap<QString, QString> m_faceIdMapping;
QHash<QString, QString> m_facePhotoLocalUrls;
QStringList m_facePhotoIds;
bool m_studentsLoadedFromServer = false;
bool m_mappingSaveInProgress = false;
QMap<QString, QString> m_pendingClassMap;
QVector<FaceListModel::Item> m_pendingFaceItems;
};
#endif
+28 -11
View File
@@ -2,6 +2,24 @@
#include "crypto/tiny_aes.hpp"
namespace {
const uint8_t AesKey[AES_KEYLEN] = {
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c
};
const uint8_t AesIv[AES_BLOCKLEN] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
};
QByteArray addPkcs7Padding(const QByteArray &data)
{
const int padding = AES_BLOCKLEN - (data.size() % AES_BLOCKLEN);
QByteArray padded = data;
padded.append(QByteArray(padding, static_cast<char>(padding)));
return padded;
}
QByteArray removePkcs7Padding(const QByteArray &data)
{
if (data.isEmpty()) {
@@ -23,18 +41,17 @@ QByteArray removePkcs7Padding(const QByteArray &data)
}
}
QByteArray CryptoUtils::encryptAes128CbcBase64(const QByteArray &plainData)
{
QByteArray buffer = addPkcs7Padding(plainData);
AES_ctx ctx;
AES_init_ctx_iv(&ctx, AesKey, AesIv);
AES_CBC_encrypt_buffer(&ctx, reinterpret_cast<uint8_t*>(buffer.data()), static_cast<size_t>(buffer.size()));
return buffer.toBase64();
}
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 {};
@@ -42,7 +59,7 @@ QByteArray CryptoUtils::decryptAes128CbcBase64(const QByteArray &encryptedBase64
QByteArray buffer = encrypted;
AES_ctx ctx;
AES_init_ctx_iv(&ctx, key, iv);
AES_init_ctx_iv(&ctx, AesKey, AesIv);
AES_CBC_decrypt_buffer(&ctx, reinterpret_cast<uint8_t*>(buffer.data()), static_cast<size_t>(buffer.size()));
return removePkcs7Padding(buffer);
}
+1
View File
@@ -6,6 +6,7 @@
class CryptoUtils
{
public:
static QByteArray encryptAes128CbcBase64(const QByteArray &plainData);
static QByteArray decryptAes128CbcBase64(const QByteArray &encryptedBase64);
};
+81
View File
@@ -176,6 +176,76 @@ static void InvMixColumns(uint8_t *state)
}
}
static void SubBytes(uint8_t *state)
{
for (uint8_t i = 0; i < 16; ++i) {
state[i] = getSBoxValue(state[i]);
}
}
static void ShiftRows(uint8_t *state)
{
uint8_t temp;
temp = state[1];
state[1] = state[5];
state[5] = state[9];
state[9] = state[13];
state[13] = temp;
temp = state[2];
state[2] = state[10];
state[10] = temp;
temp = state[6];
state[6] = state[14];
state[14] = temp;
temp = state[15];
state[15] = state[11];
state[11] = state[7];
state[7] = state[3];
state[3] = temp;
}
static void MixColumns(uint8_t *state)
{
for (uint8_t i = 0; i < 4; ++i) {
const uint8_t t = state[i * 4 + 0];
const uint8_t tmp = state[i * 4 + 0] ^ state[i * 4 + 1] ^ state[i * 4 + 2] ^ state[i * 4 + 3];
uint8_t tm = state[i * 4 + 0] ^ state[i * 4 + 1];
tm = xtime(tm);
state[i * 4 + 0] ^= tm ^ tmp;
tm = state[i * 4 + 1] ^ state[i * 4 + 2];
tm = xtime(tm);
state[i * 4 + 1] ^= tm ^ tmp;
tm = state[i * 4 + 2] ^ state[i * 4 + 3];
tm = xtime(tm);
state[i * 4 + 2] ^= tm ^ tmp;
tm = state[i * 4 + 3] ^ t;
tm = xtime(tm);
state[i * 4 + 3] ^= tm ^ tmp;
}
}
inline void Cipher(uint8_t *state, const uint8_t *RoundKey)
{
AddRoundKey(0, state, RoundKey);
for (uint8_t round = 1; round < 10; ++round) {
SubBytes(state);
ShiftRows(state);
MixColumns(state);
AddRoundKey(round, state, RoundKey);
}
SubBytes(state);
ShiftRows(state);
AddRoundKey(10, state, RoundKey);
}
inline void InvCipher(uint8_t *state, const uint8_t *RoundKey)
{
uint8_t round = 0;
@@ -198,6 +268,17 @@ inline void AES_init_ctx_iv(struct AES_ctx *ctx, const uint8_t *key, const uint8
memcpy(ctx->Iv, iv, AES_BLOCKLEN);
}
inline void AES_CBC_encrypt_buffer(struct AES_ctx *ctx, uint8_t *buf, size_t length)
{
for (size_t i = 0; i < length; i += AES_BLOCKLEN) {
for (uint8_t j = 0; j < AES_BLOCKLEN; ++j) {
buf[i + j] ^= ctx->Iv[j];
}
Cipher(buf + i, ctx->RoundKey);
memcpy(ctx->Iv, buf + i, AES_BLOCKLEN);
}
}
inline void AES_CBC_decrypt_buffer(struct AES_ctx *ctx, uint8_t *buf, size_t length)
{
size_t i;