Stream and validate native feature loading with fast decoding and bounded SDK batches. Reconcile authoritative department snapshots with rollback-safe metadata and file updates. Bound downloads, isolate duplicate identifiers, and defer unchanged invalid photo versions. Remove unused legacy face APIs and document device measurements. Validation: 42 Flutter tests in an isolated source snapshot; native unit and device checks; all 5,537 registered features verified byte-for-byte, plus real SDK photo searches and metadata checks.
206 lines
7.5 KiB
Dart
206 lines
7.5 KiB
Dart
import 'package:dio/dio.dart' as dio;
|
|
|
|
import 'package:pj_supermarket_cloud/common/index.dart';
|
|
|
|
abstract class UserApi {
|
|
/// 硬件管理登录
|
|
static Future<dynamic> login(dynamic pa) async {
|
|
var password = pa["password"];
|
|
var pwdForWire = LoginVersion.v1 == Constants.loginVersion
|
|
? password
|
|
: EncryptionTool.encryptLoginPassword(password);
|
|
final res = await HttpManage().dio.post(
|
|
"/auth/oauth2/token",
|
|
queryParameters: {
|
|
"username": pa["username"],
|
|
"randomStr": "",
|
|
"code": "",
|
|
"grant_type": pa["grant_type"],
|
|
"scope": pa["scope"],
|
|
},
|
|
data: {
|
|
"password": pwdForWire,
|
|
},
|
|
options: dio.Options(
|
|
headers: {
|
|
"Authorization": EncryptionTool.getLoginAuthorization(),
|
|
"skipToken": true,
|
|
"content-type": "application/x-www-form-urlencoded",
|
|
},
|
|
),
|
|
);
|
|
return res.data;
|
|
}
|
|
|
|
/// 刷新token
|
|
static Future<ServiceBase<ServiceAuth>> refreshToken(dynamic pa) async {
|
|
final res = await HttpManage().dio.post(
|
|
"/refresh-token",
|
|
queryParameters: pa,
|
|
);
|
|
if (res.data?.data == null) {
|
|
return res.data;
|
|
}
|
|
final map = res.data!.data as Map<String, dynamic>;
|
|
final typeData = ServiceAuth.fromJson(map);
|
|
res.data?.data = typeData;
|
|
return res.data;
|
|
}
|
|
|
|
/// 获取人脸v2
|
|
static Future<List<ServiceAsyncFace>?> getFaceListV2(
|
|
dynamic formData,
|
|
) async {
|
|
final res = await HttpManage().dio.get(
|
|
"/admin/user-face/ad/v2/find-face",
|
|
queryParameters: formData,
|
|
options: dio.Options(extra: {
|
|
HttpManage.rejectBusinessErrorKey: true,
|
|
HttpManage.responseSummaryKey: true,
|
|
}),
|
|
);
|
|
if (res.data == null) return null;
|
|
return res.data
|
|
?.map<ServiceAsyncFace>((e) => ServiceAsyncFace.fromJson(e))
|
|
.toList();
|
|
}
|
|
|
|
/// 按固定主键上界游标读取本部门及子部门的 V3 人脸权威快照。
|
|
static Future<ServiceFaceSyncSnapshot> getFaceSyncSnapshot(
|
|
Map<String, dynamic> requestBody,
|
|
) async {
|
|
final res = await HttpManage().dio.post(
|
|
"/admin/user-face/ad/v3/sync-snapshot",
|
|
data: requestBody,
|
|
options: dio.Options(extra: {
|
|
HttpManage.rejectBusinessErrorKey: true,
|
|
HttpManage.responseSummaryKey: true,
|
|
}),
|
|
);
|
|
if (res.data is! Map) {
|
|
throw const FormatException('服务端未返回有效的人脸快照');
|
|
}
|
|
return ServiceFaceSyncSnapshot.fromJson(
|
|
Map<String, dynamic>.from(res.data as Map),
|
|
);
|
|
}
|
|
|
|
/// 使用服务端真实部门树确定删除范围,包含当前已无人脸的空部门。
|
|
static Future<Set<String>> getFaceDepartmentScope(
|
|
String merchantDeptId) async {
|
|
final res = await HttpManage().dio.get(
|
|
'/admin/dept/getDescendantList/$merchantDeptId',
|
|
options: dio.Options(extra: {
|
|
HttpManage.rejectBusinessErrorKey: true,
|
|
HttpManage.responseSummaryKey: true
|
|
}),
|
|
);
|
|
if (res.data is! List) throw const FaceSyncSnapshotException('部门范围响应无效');
|
|
final ids = <String>{};
|
|
for (final item in res.data as List) {
|
|
if (item is! Map || item['deptId'] == null) {
|
|
throw const FaceSyncSnapshotException('部门范围包含无效记录');
|
|
}
|
|
final id = item['deptId'].toString();
|
|
if (int.tryParse(id) == null || !ids.add(id)) {
|
|
throw const FaceSyncSnapshotException('部门范围包含无效或重复编号');
|
|
}
|
|
}
|
|
if (!ids.contains(merchantDeptId)) {
|
|
throw const FaceSyncSnapshotException('绑定部门不存在,已停止人脸同步');
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
/// 由当前设备请求删除服务端部门范围内的人脸,单批最多 500 条。
|
|
///
|
|
/// 服务端会以 DEVICE-ID 解析真实绑定部门;业务错误必须抛给调用方,确保终端
|
|
/// 在未得到 deleted 或 alreadyAbsent 确认前不清理本地数据。
|
|
static Future<ServiceTerminalFaceDeleteResult> deleteTerminalFaces(
|
|
List<String> sampleIds,
|
|
) async {
|
|
final setting = await AppSettingTool.getSysSetting();
|
|
final hardId = setting.serviceId;
|
|
if (hardId == null) throw StateError('终端尚未绑定设备');
|
|
final res = await HttpManage().dio.post(
|
|
'/hard/hard-api/ad/face/delete',
|
|
data: {'sampleIds': sampleIds},
|
|
options: dio.Options(
|
|
headers: {'DEVICE-ID': hardId},
|
|
extra: {HttpManage.rejectBusinessErrorKey: true},
|
|
),
|
|
);
|
|
if (res.data is! Map) {
|
|
throw const FormatException('服务端未返回有效的人脸删除结果');
|
|
}
|
|
return ServiceTerminalFaceDeleteResult.fromJson(
|
|
Map<String, dynamic>.from(res.data as Map),
|
|
);
|
|
}
|
|
|
|
/// 由当前设备批量上报在库人脸静态图片质量,单批最多 500 条。
|
|
///
|
|
/// 服务端通过 DEVICE-ID 解析可信设备和部门;业务失败必须抛出,但调用方应将
|
|
/// 质量上报与既有人脸同步、注册和识别结果隔离,网络失败不得回滚本地质量。
|
|
static Future<ServiceFaceQualityReportResult> reportTerminalFaceQuality(
|
|
List<ServiceFaceQualityReportItem> items,
|
|
) async {
|
|
if (items.isEmpty) throw ArgumentError.value(items, 'items', '质量结果不能为空');
|
|
if (items.length > 500) {
|
|
throw ArgumentError.value(items.length, 'items', '单批最多上报500条质量结果');
|
|
}
|
|
final setting = await AppSettingTool.getSysSetting();
|
|
final hardId = setting.serviceId;
|
|
if (hardId == null) throw StateError('终端尚未绑定设备');
|
|
final res = await HttpManage().dio.post(
|
|
'/hard/hard-api/ad/face/quality/report',
|
|
data: {
|
|
'items': items.map((item) => item.toJson()).toList(growable: false),
|
|
},
|
|
options: dio.Options(
|
|
headers: {'DEVICE-ID': hardId},
|
|
extra: {HttpManage.rejectBusinessErrorKey: true},
|
|
),
|
|
);
|
|
if (res.data is! Map) {
|
|
throw const FormatException('服务端未返回有效的人脸质量上报结果');
|
|
}
|
|
return ServiceFaceQualityReportResult.fromJson(
|
|
Map<String, dynamic>.from(res.data as Map),
|
|
);
|
|
}
|
|
|
|
/// 竞争当前商户部门的人脸质量上报租约。
|
|
///
|
|
/// Hardware 仍会在实际上报时二次验证租约;未取得租约不是业务失败,调用方应
|
|
/// 直接跳过本轮质量批次,避免同一单位的多台终端重复发送相同在库结果。
|
|
static Future<ServiceFaceQualityLease>
|
|
acquireTerminalFaceQualityLease() async {
|
|
final setting = await AppSettingTool.getSysSetting();
|
|
final hardId = setting.serviceId;
|
|
if (hardId == null) throw StateError('终端尚未绑定设备');
|
|
final res = await HttpManage().dio.post(
|
|
'/hard/hard-api/ad/face/quality/lease',
|
|
options: dio.Options(
|
|
headers: {'DEVICE-ID': hardId},
|
|
extra: {HttpManage.rejectBusinessErrorKey: true},
|
|
),
|
|
);
|
|
if (res.data is! Map) {
|
|
throw const FormatException('服务端未返回有效的人脸质量租约');
|
|
}
|
|
return ServiceFaceQualityLease.fromJson(
|
|
Map<String, dynamic>.from(res.data as Map),
|
|
);
|
|
}
|
|
|
|
/// 获取租户列表
|
|
static Future<List<ServiceTenant>?> getTenantList() async {
|
|
final res = await HttpManage().dio.get("/admin/tenant/list");
|
|
if (res.data == null) return null;
|
|
return res.data
|
|
?.map<ServiceTenant>((e) => ServiceTenant.fromJson(e))
|
|
.toList();
|
|
}
|
|
}
|