Files
ttstd_family_care/lib/features/device/data/device_api.dart
2026-08-21 17:12:17 +08:00

231 lines
8.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:cupertino_ui/cupertino_ui.dart';
import 'package:ttstd_family_care/app/constants/app_constants.dart';
import 'package:ttstd_family_care/core/network/api_exception.dart';
import 'package:ttstd_family_care/core/network/dio_client.dart';
import 'package:ttstd_family_care/features/device/domain/device_models.dart';
/// 客户端(家属端)设备接口访问层。
///
/// 后端接口前缀为 `/api/v1/client/sn`(与登录接口所在的 `/api/v1/open` 不同),
/// 由于 Dio 在 path 以 `/` 开头时会替换 baseUrl 的完整 path 段(保留 host
/// 这里直接使用绝对路径即可正确命中 client 接口。
/// [DioClient.get] 已统一解析为后台返回体中的 `data` 字段;业务失败时抛出
/// [ApiException],本层将其降级为「空数据」以适配家属端展示。
class DeviceApi {
const DeviceApi();
/// 获取当前用户已绑定的设备列表。
Future<List<DeviceBrief>> getMyDevices() async {
try {
final data = await DioClient.get(
'v1/client/sn/my-devices',
);
return _parseList(data, DeviceBrief.fromJson);
} on ApiException {
return const [];
}
}
/// 绑定设备(手动输入 SN 或扫码得到 SN 后调用)。
///
/// 成功返回 null失败返回后台错误提示。
Future<String?> bindDevice(String sn, {String? remark}) async {
try {
final data = await DioClient.post(
'v1/client/sn/bind',
queryParameters: {'sn': sn},
data: remark != null && remark.isNotEmpty ? {'remark': remark} : null,
);
debugPrint('[DeviceApi] bindDevice sn=$sn data=$data');
return null;
} on ApiException catch (e) {
debugPrint('[DeviceApi] bindDevice sn=$sn failed: ${e.message}');
return e.message;
}
}
/// 解绑设备。
///
/// 成功返回 null失败返回后台错误提示。
Future<String?> unbindDevice(String sn) async {
try {
final data = await DioClient.post(
'v1/client/sn/unbind',
queryParameters: {'sn': sn},
);
debugPrint('[DeviceApi] unbindDevice sn=$sn data=$data');
return null;
} on ApiException catch (e) {
debugPrint('[DeviceApi] unbindDevice sn=$sn failed: ${e.message}');
return e.message;
}
}
/// 获取已绑定设备基本信息(无上报数据时返回 null
Future<DeviceSystemInfo?> getDeviceInfo(String sn) async {
try {
final data = await DioClient.get(
'v1/client/sn/device-info',
queryParameters: {'sn': sn},
);
return data is Map<String, dynamic> ? DeviceSystemInfo.fromJson(data) : null;
} on ApiException {
return null;
}
}
/// 获取已绑定设备最新定位信息(无上报数据时返回 null
Future<DeviceLocation?> getLocation(String sn) async {
try {
final data = await DioClient.get(
'v1/client/sn/location',
queryParameters: {'sn': sn},
);
return data is Map<String, dynamic> ? DeviceLocation.fromJson(data) : null;
} on ApiException {
return null;
}
}
/// 获取已绑定设备已安装应用列表。
Future<List<DeviceApkInfo>> getApks(String sn) async {
try {
final data = await DioClient.get(
'v1/client/sn/apks',
queryParameters: {'sn': sn},
);
return _parseList(data, DeviceApkInfo.fromJson);
} on ApiException {
return const [];
}
}
/// 向设备下发操作指令(重启/关机/截屏/刷新/定位等)。
///
/// [op] 为后端操作标识,如 `reboot` / `shutdown` / `screenshot` / `refresh` / `locate`
/// 对应接口路径 `/api/v1/client/sn/ops/{op}?sn=xxx`。
/// 成功返回 null失败返回后台错误提示。
Future<String?> sendDeviceOp(String sn, String op) async {
try {
final data = await DioClient.post(
'v1/client/sn/ops/$op',
queryParameters: {'sn': sn},
);
debugPrint('[DeviceApi] sendDeviceOp op=$op sn=$sn data=$data');
return null;
} on ApiException catch (e) {
debugPrint('[DeviceApi] sendDeviceOp op=$op sn=$sn failed: ${e.message}');
return e.message;
}
}
/// 对指定应用执行操作(打开/停止/卸载/清除数据)。
///
/// [op] 为后端操作标识:`launch` / `stop` / `uninstall` / `clear_data`
/// 对应接口路径 `/api/v1/client/sn/ops/app/{op}?sn=xxx`,请求体携带应用包名。
/// 成功返回 null失败返回后台错误提示。
Future<String?> sendAppOp(String sn, String op, String packageName) async {
try {
final data = await DioClient.post(
'v1/client/sn/ops/app/$op',
queryParameters: {'sn': sn},
data: {'packageName': packageName},
);
debugPrint('[DeviceApi] sendAppOp op=$op sn=$sn pkg=$packageName data=$data');
return null;
} on ApiException catch (e) {
debugPrint('[DeviceApi] sendAppOp op=$op sn=$sn pkg=$packageName failed: ${e.message}');
return e.message;
}
}
// ---------------------------------------------------------------------------
static List<T> _parseList<T>(
dynamic data,
T Function(Map<String, dynamic>) fromJson,
) {
if (data is! List) return const [];
return data
.whereType<Map<String, dynamic>>()
.map(fromJson)
.toList(growable: false);
}
// ---------------------------------------------------------------------------
/// 获取设备最近截图列表(家属端)。
///
/// [sn] 为设备序列号(可空,由后台按绑定关系补全)。
/// 返回 [ScreenshotVO] 列表,并自动补全图片可访问的完整 URL拼接 baseUrl
Future<List<ScreenshotVO>> getRecentScreenshots({String? sn}) async {
final query = sn != null && sn.isNotEmpty ? {'sn': sn} : <String, dynamic>{};
// DioClient.get 已统一解析出响应体中的 data 字段,
// 后台返回结构为 { code, msg, data: [...] },故此处 payload 即为截图数组。
final payload = await DioClient.get(
'v1/client/sn/upload-screenshot',
queryParameters: query,
);
// 防御性提取:兼容后台直接返回 List或返回 { data: [...] } 的包裹结构。
final List<dynamic> listData;
if (payload is List) {
listData = payload;
} else if (payload is Map && payload['data'] is List) {
listData = payload['data'] as List;
} else {
debugPrint('[DeviceApi] getRecentScreenshots 返回非预期结构: sn=$sn payload=$payload');
return const [];
}
debugPrint('[DeviceApi] getRecentScreenshots sn=$sn count=${listData.length}');
final origin = Uri.parse(AppConstants.kBaseUrl).origin;
return listData.whereType<Map<String, dynamic>>().map((e) {
final vo = ScreenshotVO.fromJson(e);
// 后台返回的 url 为相对路径(如 /static/screenshot/xxx补全为完整地址。
final raw = vo.url;
final fullUrl = (raw != null && raw.startsWith('/'))
? '$origin$raw'
: (raw ?? '');
return ScreenshotVO(
id: vo.id,
sn: vo.sn,
fileName: vo.fileName,
fileSize: vo.fileSize,
url: fullUrl,
uploadTime: vo.uploadTime,
);
}).toList(growable: false);
}
/// 删除单张设备截图(家属端 client 接口)。
///
/// [sn] 设备序列号,[id] 截图ID。成功返回 null失败返回后台错误提示。
Future<String?> deleteScreenshot(String sn, int id) async {
try {
await DioClient.delete(
'v1/client/sn/ops/screenshots/$id?sn=${Uri.encodeQueryComponent(sn)}',
);
debugPrint('[DeviceApi] deleteScreenshot sn=$sn id=$id success');
return null;
} on ApiException catch (e) {
debugPrint('[DeviceApi] deleteScreenshot sn=$sn id=$id failed: ${e.message}');
return e.message;
}
}
/// 清空设备全部截图(家属端 client 接口)。
///
/// [sn] 设备序列号。成功返回 null失败返回后台错误提示。
Future<String?> clearScreenshots(String sn) async {
try {
await DioClient.delete(
'v1/client/sn/ops/screenshots?sn=${Uri.encodeQueryComponent(sn)}',
);
debugPrint('[DeviceApi] clearScreenshots sn=$sn success');
return null;
} on ApiException catch (e) {
debugPrint('[DeviceApi] clearScreenshots sn=$sn failed: ${e.message}');
return e.message;
}
}
}