Initial QT project upload
This commit is contained in:
@@ -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> ¶ms) 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 ×tamp, 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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user