终版
This commit is contained in:
@@ -16,14 +16,17 @@ namespace {
|
||||
constexpr int NetworkTimeoutMs = 5000;
|
||||
}
|
||||
|
||||
// 函数作用:初始化网络客户端,并设置默认服务器地址和 API Token。
|
||||
ApiClient::ApiClient(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_serverUrl("http://27.106.113.205:8082")
|
||||
// Token 优先从环境读取,便于发布或调试时不把真实 Token 写死在配置文件里。
|
||||
, m_apiToken(QProcessEnvironment::systemEnvironment()
|
||||
.value(QStringLiteral("atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV")))
|
||||
{
|
||||
}
|
||||
|
||||
// 函数作用:更新服务端基础地址,忽略空字符串。
|
||||
void ApiClient::setServerUrl(const QString &url)
|
||||
{
|
||||
const QString trimmed = url.trimmed();
|
||||
@@ -32,11 +35,13 @@ void ApiClient::setServerUrl(const QString &url)
|
||||
}
|
||||
}
|
||||
|
||||
// 函数作用:返回当前服务端基础地址。
|
||||
QString ApiClient::serverUrl() const
|
||||
{
|
||||
return m_serverUrl;
|
||||
}
|
||||
|
||||
// 函数作用:更新 API Token,空值时回退读取环境变量。
|
||||
void ApiClient::setApiToken(const QString &token)
|
||||
{
|
||||
const QString trimmed = token.trimmed();
|
||||
@@ -46,14 +51,17 @@ void ApiClient::setApiToken(const QString &token)
|
||||
: trimmed;
|
||||
}
|
||||
|
||||
// 函数作用:返回当前 API Token。
|
||||
QString ApiClient::apiToken() const
|
||||
{
|
||||
return m_apiToken;
|
||||
}
|
||||
|
||||
// 函数作用:拼接完整 API 地址,并编码查询参数。
|
||||
QString ApiClient::buildUrl(const QString &endpoint, const QMap<QString, QString> ¶ms) const
|
||||
{
|
||||
QString baseUrl = m_serverUrl.trimmed();
|
||||
// 统一去掉用户输入末尾的斜杠,避免拼接出 http://host//api 这类地址。
|
||||
while (baseUrl.endsWith('/')) {
|
||||
baseUrl.chop(1);
|
||||
}
|
||||
@@ -68,15 +76,18 @@ QString ApiClient::buildUrl(const QString &endpoint, const QMap<QString, QString
|
||||
return url;
|
||||
}
|
||||
|
||||
// 函数作用:创建带认证头、加密标记和超时时间的网络请求对象。
|
||||
QNetworkRequest ApiClient::createRequest(const QString &url, bool encryptedResponse, bool jsonBody) const
|
||||
{
|
||||
QNetworkRequest request{QUrl(url)};
|
||||
if (encryptedResponse) {
|
||||
// 告诉服务端此请求希望接收加密响应。实际是否加密以响应头 X-Encrypted 为准。
|
||||
request.setRawHeader("X-Encrypted-Response", "true");
|
||||
}
|
||||
request.setRawHeader("X-Api-Token", m_apiToken.toUtf8());
|
||||
request.setRawHeader("Accept", "application/json");
|
||||
if (jsonBody) {
|
||||
// 服务端约定加密后的 JSON 以 octet-stream 传输,并通过 X-Encrypted 声明算法。
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream");
|
||||
request.setRawHeader("X-Encrypted", "AES128-CBC");
|
||||
}
|
||||
@@ -84,8 +95,10 @@ QNetworkRequest ApiClient::createRequest(const QString &url, bool encryptedRespo
|
||||
return request;
|
||||
}
|
||||
|
||||
// 函数作用:请求服务端同步状态,根据版本号决定是否返回完整业务数据。
|
||||
void ApiClient::fetchSyncState(qulonglong lastVersion)
|
||||
{
|
||||
// 统一同步入口:服务端会比较客户端版本,若 changed=false,客户端无需重复拉取各表。
|
||||
const QString url = buildUrl("/v1/sync/state", {
|
||||
{"client", "qt"},
|
||||
{"last_version", QString::number(lastVersion)}
|
||||
@@ -105,6 +118,7 @@ void ApiClient::fetchSyncState(qulonglong lastVersion)
|
||||
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;
|
||||
@@ -112,6 +126,8 @@ void ApiClient::fetchSyncState(qulonglong lastVersion)
|
||||
|
||||
const QJsonObject state = root.value("state").toObject();
|
||||
|
||||
// state 中按需包含多个业务分区;每个分区解析后直接复用已有信号,
|
||||
// 让 AppController 不必区分“增量同步返回”还是“单独接口返回”。
|
||||
if (state.contains("records")) {
|
||||
emit recordsFetched(parseRecords(QJsonDocument(state.value("records").toArray()).toJson(QJsonDocument::Compact)));
|
||||
}
|
||||
@@ -143,6 +159,7 @@ void ApiClient::fetchSyncState(qulonglong lastVersion)
|
||||
QMap<QString, QString> studentClassMap;
|
||||
QMap<QString, QString> classMap;
|
||||
|
||||
// 学生数组先拆成“学号->姓名”和“学号->班级”两张映射,保持与本地模型结构一致。
|
||||
const QJsonArray studentArray = state.value("students").toArray();
|
||||
for (const QJsonValue &value : studentArray) {
|
||||
const QJsonObject object = value.toObject();
|
||||
@@ -155,6 +172,7 @@ void ApiClient::fetchSyncState(qulonglong lastVersion)
|
||||
}
|
||||
|
||||
const QJsonArray classes = state.value("classes").toArray();
|
||||
// 班级数组单独解析,后面用 class_id 回填学生档案中的班级名称。
|
||||
for (const QJsonValue &value : classes) {
|
||||
const QJsonObject object = value.toObject();
|
||||
const QString classId = object.value("class_id").toString().trimmed();
|
||||
@@ -164,6 +182,7 @@ void ApiClient::fetchSyncState(qulonglong lastVersion)
|
||||
}
|
||||
QVector<StudentProfileListModel::Item> studentItems;
|
||||
studentItems.reserve(studentMap.size());
|
||||
// ApiClient 只做结构转换,是否保存到本地、是否刷新统计由 AppController 决定。
|
||||
for (auto it = studentMap.cbegin(); it != studentMap.cend(); ++it) {
|
||||
const QString studentId = it.key();
|
||||
studentItems.push_back({
|
||||
@@ -183,6 +202,7 @@ void ApiClient::fetchSyncState(qulonglong lastVersion)
|
||||
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();
|
||||
@@ -207,6 +227,7 @@ void ApiClient::fetchSyncState(qulonglong lastVersion)
|
||||
if (state.contains("face_photos")) {
|
||||
QStringList ids;
|
||||
const QJsonArray photos = state.value("face_photos").toArray();
|
||||
// 同步状态只返回照片 ID 列表,真正的 JPEG 数据由控制器按需逐张下载。
|
||||
for (const QJsonValue &value : photos) {
|
||||
ids.push_back(value.toObject().value("id").toString());
|
||||
}
|
||||
@@ -218,18 +239,22 @@ void ApiClient::fetchSyncState(qulonglong lastVersion)
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:根据响应头判断是否需要解密响应体,并返回可解析的数据。
|
||||
QByteArray ApiClient::maybeDecrypt(const QByteArray &data, bool encrypted) const
|
||||
{
|
||||
if (!encrypted) {
|
||||
return data;
|
||||
}
|
||||
const QByteArray trimmed = data.trimmed();
|
||||
// 调试或兼容场景下,服务端可能带了加密头但直接返回明文 JSON;
|
||||
// 这里先识别 JSON 起始字符,避免对明文重复解密导致解析失败。
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
return trimmed;
|
||||
}
|
||||
return CryptoUtils::decryptAes128CbcBase64(trimmed);
|
||||
}
|
||||
|
||||
// 函数作用:读取 QNetworkReply 响应体,并按响应头自动解密。
|
||||
QByteArray ApiClient::decryptedReplyBody(QNetworkReply *reply) const
|
||||
{
|
||||
const QByteArray data = reply->readAll();
|
||||
@@ -237,13 +262,16 @@ QByteArray ApiClient::decryptedReplyBody(QNetworkReply *reply) const
|
||||
return maybeDecrypt(data, encrypted);
|
||||
}
|
||||
|
||||
// 函数作用:把 JSON 请求体按服务端约定加密后发送。
|
||||
QByteArray ApiClient::encryptedJsonBody(const QByteArray &body) const
|
||||
{
|
||||
return CryptoUtils::encryptAes128CbcBase64(body);
|
||||
}
|
||||
|
||||
// 函数作用:判断网络错误是否属于连接不可用,需要触发重连。
|
||||
bool ApiClient::isConnectionError(QNetworkReply::NetworkError error) const
|
||||
{
|
||||
// 这些错误大多表示服务端不可达或连接中断,需要通知 AppController 进入重连流程。
|
||||
switch (error) {
|
||||
case QNetworkReply::ConnectionRefusedError:
|
||||
case QNetworkReply::RemoteHostClosedError:
|
||||
@@ -267,14 +295,17 @@ bool ApiClient::isConnectionError(QNetworkReply::NetworkError error) const
|
||||
}
|
||||
}
|
||||
|
||||
// 函数作用:统一发出网络错误信号,并在连接类错误时额外通知重连流程。
|
||||
void ApiClient::emitNetworkError(const QString &prefix, QNetworkReply *reply)
|
||||
{
|
||||
// networkError 负责界面提示,connectionUnavailable 负责触发自动重连。
|
||||
emit networkError(prefix);
|
||||
if (isConnectionError(reply->error())) {
|
||||
emit connectionUnavailable(prefix);
|
||||
}
|
||||
}
|
||||
|
||||
// 函数作用:解析服务端返回的考勤记录 JSON 数组。
|
||||
QVector<AttendanceRecord> ApiClient::parseRecords(const QByteArray &data) const
|
||||
{
|
||||
QVector<AttendanceRecord> records;
|
||||
@@ -285,9 +316,11 @@ QVector<AttendanceRecord> ApiClient::parseRecords(const QByteArray &data) const
|
||||
|
||||
const QJsonArray array = doc.array();
|
||||
records.reserve(array.size());
|
||||
// 服务端历史数据中 status 可能是 bool、数字或字符串,统一转换为字符串保存。
|
||||
for (const QJsonValue &value : array) {
|
||||
const QJsonObject object = value.toObject();
|
||||
AttendanceRecord record;
|
||||
// 只保留服务端的原始业务字段;姓名和班级显示值稍后由 AppController 映射生成。
|
||||
record.timestamp = object.value("timestamp").toString().trimmed();
|
||||
record.studentId = object.value("student_id").toString().trimmed();
|
||||
record.originalName = object.value("name").toString().trimmed();
|
||||
@@ -304,6 +337,7 @@ QVector<AttendanceRecord> ApiClient::parseRecords(const QByteArray &data) const
|
||||
if (record.classId.isEmpty()) {
|
||||
record.classId = "0";
|
||||
}
|
||||
// 过滤掉初始化时间和缺失关键字段的无效记录,避免界面统计被脏数据污染。
|
||||
if (!record.timestamp.startsWith("0000-") && !record.timestamp.isEmpty() && !record.studentId.isEmpty()) {
|
||||
records.push_back(record);
|
||||
}
|
||||
@@ -311,8 +345,10 @@ QVector<AttendanceRecord> ApiClient::parseRecords(const QByteArray &data) const
|
||||
return records;
|
||||
}
|
||||
|
||||
// 函数作用:分页请求考勤记录列表。
|
||||
void ApiClient::fetchRecords(int limit, int offset)
|
||||
{
|
||||
// 考勤记录分页接口。当前管理端默认一次取最近 1000 条,后续可通过 limit/offset 扩展分页。
|
||||
const QString url = buildUrl("/v1/attendance/records", {
|
||||
{"limit", QString::number(limit)},
|
||||
{"offset", QString::number(offset)}
|
||||
@@ -338,6 +374,7 @@ void ApiClient::fetchRecords(int limit, int offset)
|
||||
return;
|
||||
}
|
||||
|
||||
// 先显式验证 JSON 类型,避免 parseRecords 静默返回空列表时界面误以为服务端真的没有记录。
|
||||
QJsonParseError jsonError{};
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
|
||||
if (jsonError.error != QJsonParseError::NoError || !doc.isArray()) {
|
||||
@@ -353,6 +390,7 @@ void ApiClient::fetchRecords(int limit, int offset)
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:请求服务端考勤统计摘要。
|
||||
void ApiClient::fetchStatistics()
|
||||
{
|
||||
const QString url = buildUrl("/v1/attendance/statistics");
|
||||
@@ -386,6 +424,7 @@ void ApiClient::fetchStatistics()
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:请求服务端清空全部考勤记录。
|
||||
void ApiClient::clearAllRecords()
|
||||
{
|
||||
QNetworkReply *reply = m_networkManager.post(createRequest(buildUrl("/v1/attendance/records/clear"), true, true),
|
||||
@@ -407,6 +446,7 @@ void ApiClient::clearAllRecords()
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:根据时间戳和学号删除一条考勤记录。
|
||||
void ApiClient::deleteRecord(const QString ×tamp, const QString &studentId)
|
||||
{
|
||||
QNetworkReply *reply = m_networkManager.deleteResource(createRequest(buildUrl("/v1/attendance/records", {
|
||||
@@ -430,6 +470,7 @@ void ApiClient::deleteRecord(const QString ×tamp, const QString &studentId)
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:从服务端加载签到时间段配置。
|
||||
void ApiClient::fetchTimeSlots()
|
||||
{
|
||||
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/config/time-slots"), true));
|
||||
@@ -444,6 +485,7 @@ void ApiClient::fetchTimeSlots()
|
||||
QVector<TimeSlot> timeSlots;
|
||||
const QJsonObject root = QJsonDocument::fromJson(data).object();
|
||||
const QJsonArray array = root.value("time_slots").toArray();
|
||||
// 时间使用 HH:mm 字符串传输,进入模型前转换为 QTime,便于后续编辑和格式化。
|
||||
for (const QJsonValue &value : array) {
|
||||
const QJsonObject object = value.toObject();
|
||||
timeSlots.push_back({
|
||||
@@ -458,9 +500,11 @@ void ApiClient::fetchTimeSlots()
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:上传签到时间段配置到服务端。
|
||||
void ApiClient::uploadTimeSlots(const QVector<TimeSlot> &timeSlots)
|
||||
{
|
||||
QJsonArray array;
|
||||
// 字段名与服务端 Flask 接口保持一致:name/start_time/end_time/enabled。
|
||||
for (const TimeSlot &slot : timeSlots) {
|
||||
QJsonObject object;
|
||||
object["name"] = slot.name;
|
||||
@@ -473,6 +517,7 @@ void ApiClient::uploadTimeSlots(const QVector<TimeSlot> &timeSlots)
|
||||
QJsonObject payload;
|
||||
payload["time_slots"] = array;
|
||||
const QByteArray body = QJsonDocument(payload).toJson(QJsonDocument::Compact);
|
||||
// 日志打印明文 JSON 只用于本地调试,真正发送前仍会在 encryptedJsonBody 中加密。
|
||||
qInfo() << "[ApiClient] POST" << buildUrl("/v1/config/time-slots")
|
||||
<< "bytes=" << body.size()
|
||||
<< "body=" << body.left(300);
|
||||
@@ -494,6 +539,7 @@ void ApiClient::uploadTimeSlots(const QVector<TimeSlot> &timeSlots)
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:从服务端加载学生档案列表。
|
||||
void ApiClient::fetchStudents()
|
||||
{
|
||||
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/students"), true));
|
||||
@@ -508,6 +554,7 @@ void ApiClient::fetchStudents()
|
||||
QVector<StudentProfileListModel::Item> students;
|
||||
const QJsonArray array = QJsonDocument::fromJson(data).object().value("students").toArray();
|
||||
students.reserve(array.size());
|
||||
// className、faceIds、bindingStatus 是控制器派生字段,接口层保持为空。
|
||||
for (const QJsonValue &value : array) {
|
||||
const QJsonObject object = value.toObject();
|
||||
students.push_back({
|
||||
@@ -524,9 +571,11 @@ void ApiClient::fetchStudents()
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:上传学生档案列表到服务端。
|
||||
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()) {
|
||||
@@ -541,6 +590,7 @@ void ApiClient::uploadStudents(const QVector<StudentProfileListModel::Item> &stu
|
||||
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]() {
|
||||
@@ -559,6 +609,7 @@ void ApiClient::uploadStudents(const QVector<StudentProfileListModel::Item> &stu
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:从服务端加载班级配置。
|
||||
void ApiClient::fetchClasses()
|
||||
{
|
||||
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/classes"), true));
|
||||
@@ -572,6 +623,7 @@ void ApiClient::fetchClasses()
|
||||
|
||||
QMap<QString, QString> classMap;
|
||||
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();
|
||||
@@ -584,9 +636,11 @@ void ApiClient::fetchClasses()
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:上传班级配置到服务端。
|
||||
void ApiClient::uploadClasses(const QMap<QString, QString> &classNameMap)
|
||||
{
|
||||
QJsonArray array;
|
||||
// QMap 会按 key 排序,上传顺序稳定,便于服务端日志和调试比对。
|
||||
for (auto it = classNameMap.cbegin(); it != classNameMap.cend(); ++it) {
|
||||
const QString classId = it.key().trimmed();
|
||||
if (classId.isEmpty()) {
|
||||
@@ -618,6 +672,7 @@ void ApiClient::uploadClasses(const QMap<QString, QString> &classNameMap)
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:从服务端加载人脸档案绑定关系。
|
||||
void ApiClient::fetchFaceProfiles()
|
||||
{
|
||||
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/face_profiles"), true));
|
||||
@@ -650,9 +705,11 @@ void ApiClient::fetchFaceProfiles()
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:上传人脸档案绑定关系到服务端。
|
||||
void ApiClient::uploadFaceProfiles(const QVector<FaceListModel::Item> &faces)
|
||||
{
|
||||
QJsonArray array;
|
||||
// 人脸编号是服务端保存模板和照片的主键;空编号跳过,避免破坏已有档案。
|
||||
for (const FaceListModel::Item &face : faces) {
|
||||
const QString faceId = face.faceId.trimmed();
|
||||
if (faceId.isEmpty()) {
|
||||
@@ -661,6 +718,7 @@ void ApiClient::uploadFaceProfiles(const QVector<FaceListModel::Item> &faces)
|
||||
QJsonObject object;
|
||||
object["face_id"] = faceId;
|
||||
object["student_id"] = face.studentId.trimmed();
|
||||
// status 根据绑定状态自动生成,界面不直接编辑该字段。
|
||||
object["status"] = face.studentId.trimmed().isEmpty() ? QStringLiteral("unbound") : QStringLiteral("active");
|
||||
array.append(object);
|
||||
}
|
||||
@@ -686,6 +744,7 @@ void ApiClient::uploadFaceProfiles(const QVector<FaceListModel::Item> &faces)
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:从服务端加载已保存照片的人脸编号列表。
|
||||
void ApiClient::fetchFacePhotos()
|
||||
{
|
||||
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/faces/photos"), true));
|
||||
@@ -699,6 +758,7 @@ void ApiClient::fetchFacePhotos()
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -707,6 +767,7 @@ void ApiClient::fetchFacePhotos()
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:按人脸编号下载单张人脸照片。
|
||||
void ApiClient::fetchFacePhoto(const QString &faceId)
|
||||
{
|
||||
if (faceId.trimmed().isEmpty()) {
|
||||
@@ -716,6 +777,7 @@ void ApiClient::fetchFacePhoto(const QString &faceId)
|
||||
|
||||
const QString encodedId = QString::fromUtf8(QUrl::toPercentEncoding(faceId.trimmed()));
|
||||
QNetworkRequest request = createRequest(buildUrl(QStringLiteral("/v1/faces/photos/%1").arg(encodedId)));
|
||||
// 照片接口返回 JPEG 二进制,不走 JSON 解密流程。
|
||||
request.setRawHeader("Accept", "image/jpeg,*/*");
|
||||
|
||||
QNetworkReply *reply = m_networkManager.get(request);
|
||||
@@ -726,6 +788,7 @@ void ApiClient::fetchFacePhoto(const QString &faceId)
|
||||
const QString message = reply->error() != QNetworkReply::NoError
|
||||
? reply->errorString()
|
||||
: QStringLiteral("HTTP %1").arg(statusCode);
|
||||
// 下载失败只影响单张照片显示,不阻断其它照片继续加载。
|
||||
emit facePhotoFetched(faceId, {}, false, message);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
@@ -736,6 +799,7 @@ void ApiClient::fetchFacePhoto(const QString &faceId)
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:删除指定学生对应的人脸数据,并可选择删除其考勤记录。
|
||||
void ApiClient::deleteFace(const QString &studentId, bool deleteRecords)
|
||||
{
|
||||
QString baseUrl = m_serverUrl.trimmed();
|
||||
@@ -744,6 +808,7 @@ void ApiClient::deleteFace(const QString &studentId, bool deleteRecords)
|
||||
}
|
||||
QString url = baseUrl + "/api/v1/faces/" + QString::fromLatin1(QUrl::toPercentEncoding(studentId));
|
||||
if (deleteRecords) {
|
||||
// delete_records=true 表示删除人脸时同步清除该学生关联的考勤记录。
|
||||
url += "?delete_records=true";
|
||||
}
|
||||
QNetworkReply *reply = m_networkManager.deleteResource(createRequest(url, true));
|
||||
@@ -767,12 +832,15 @@ void ApiClient::deleteFace(const QString &studentId, bool deleteRecords)
|
||||
});
|
||||
}
|
||||
|
||||
// 函数作用:请求服务端健康检查接口,判断当前连接是否可用。
|
||||
void ApiClient::checkHealth()
|
||||
{
|
||||
// 健康检查使用最轻量的系统接口,供启动同步、手动测试和自动重连共用。
|
||||
QNetworkReply *reply = m_networkManager.get(createRequest(buildUrl("/v1/system/health"), true));
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
const QByteArray data = decryptedReplyBody(reply);
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
// 健康检查只通过 healthChecked 返回结果,避免普通 networkError toast 反复刷屏。
|
||||
emit healthChecked(false, reply->errorString());
|
||||
reply->deleteLater();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user