2026-06-16 17:34:00 +08:00
|
|
|
|
import 'dart:convert';
|
|
|
|
|
|
import 'package:dio/dio.dart';
|
|
|
|
|
|
import 'package:get_it/get_it.dart';
|
2026-07-11 16:46:53 +08:00
|
|
|
|
import '../../../../core/consts/http_api_consts.dart';
|
2026-06-16 17:34:00 +08:00
|
|
|
|
import '../../../../core/logging/i_logger_service.dart';
|
2026-11-06 08:41:44 +08:00
|
|
|
|
import '../../domain/entities/device_schedule_plan_entity.dart';
|
2026-06-16 17:34:00 +08:00
|
|
|
|
import '../models/device_task_model.dart';
|
|
|
|
|
|
|
|
|
|
|
|
abstract class DeviceTaskDatasource {
|
|
|
|
|
|
Future<List<DeviceTaskModel>> getDeviceTaskPool({
|
|
|
|
|
|
required String userId,
|
|
|
|
|
|
required int siteId,
|
|
|
|
|
|
required int orgId,
|
|
|
|
|
|
int pageNum = 1,
|
|
|
|
|
|
int pageSize = 99999999,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Future<bool> cancelTask({
|
|
|
|
|
|
required String deviceId,
|
|
|
|
|
|
required int taskId,
|
|
|
|
|
|
required int orgId,
|
|
|
|
|
|
required int siteId,
|
2026-09-01 08:59:20 +08:00
|
|
|
|
bool forceCancel = true,
|
2026-06-16 17:34:00 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Future<bool> pauseTask({
|
|
|
|
|
|
required String deviceId,
|
|
|
|
|
|
required int taskId,
|
|
|
|
|
|
required int orgId,
|
|
|
|
|
|
required int siteId,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Future<Map<String, dynamic>> recoveryTask({
|
|
|
|
|
|
required String deviceId,
|
|
|
|
|
|
required int taskId,
|
|
|
|
|
|
required int orgId,
|
|
|
|
|
|
required int siteId,
|
|
|
|
|
|
});
|
2026-11-06 08:41:44 +08:00
|
|
|
|
|
|
|
|
|
|
/// 创建设备定时任务(定时作业计划)POST /iot/deviceTask/save
|
|
|
|
|
|
/// 周期规则报文组装封在实现内,调用方只交业务实体
|
|
|
|
|
|
Future<bool> saveDeviceSchedulePlan({required DeviceSchedulePlanEntity plan});
|
2026-06-16 17:34:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class DeviceTaskDatasourceImpl implements DeviceTaskDatasource {
|
|
|
|
|
|
final Dio dio;
|
|
|
|
|
|
final ILoggerService _logger = GetIt.I<ILoggerService>();
|
|
|
|
|
|
|
|
|
|
|
|
DeviceTaskDatasourceImpl(this.dio);
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Future<List<DeviceTaskModel>> getDeviceTaskPool({
|
|
|
|
|
|
required String userId,
|
|
|
|
|
|
required int siteId,
|
|
|
|
|
|
required int orgId,
|
|
|
|
|
|
int pageNum = 1,
|
|
|
|
|
|
int pageSize = 99999999,
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
try {
|
2026-07-11 16:46:53 +08:00
|
|
|
|
final url = '${HttpApiConsts.baseUrl}/iot/deviceTask/deviceTaskPool';
|
2026-11-06 08:41:44 +08:00
|
|
|
|
_logger.logWithLevel('[getDeviceTaskPool] 请求: POST $url', shouldLog: true);
|
|
|
|
|
|
_logger.logWithLevel('[getDeviceTaskPool] 参数: userId=$userId, siteId=$siteId, orgId=$orgId, pageNum=$pageNum, pageSize=$pageSize', shouldLog: true);
|
2026-08-07 08:49:29 +08:00
|
|
|
|
final response = await dio.post(
|
2026-06-16 17:34:00 +08:00
|
|
|
|
url,
|
2026-08-07 08:49:29 +08:00
|
|
|
|
data: {
|
2026-06-16 17:34:00 +08:00
|
|
|
|
'userId': userId,
|
|
|
|
|
|
'siteId': siteId,
|
|
|
|
|
|
'orgId': orgId,
|
|
|
|
|
|
'pageNum': pageNum,
|
|
|
|
|
|
'pageSize': pageSize,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
|
|
final data = response.data as Map<String, dynamic>;
|
|
|
|
|
|
if (data['code'] == 200) {
|
|
|
|
|
|
final rows = data['rows'] as List? ?? [];
|
2026-11-06 08:41:44 +08:00
|
|
|
|
_logger.logWithLevel('[getDeviceTaskPool] 返回任务 ${rows.length} 条', shouldLog: true);
|
2026-06-16 17:34:00 +08:00
|
|
|
|
return rows
|
|
|
|
|
|
.map((item) => DeviceTaskModel.fromJson(item as Map<String, dynamic>))
|
|
|
|
|
|
.toList();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw Exception(data['msg'] ?? '获取任务池失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw Exception('HTTP ${response.statusCode}');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
_logger.logWithLevel('❌ 获取任务池失败: $e');
|
|
|
|
|
|
rethrow;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Future<bool> cancelTask({
|
|
|
|
|
|
required String deviceId,
|
|
|
|
|
|
required int taskId,
|
|
|
|
|
|
required int orgId,
|
|
|
|
|
|
required int siteId,
|
2026-09-01 08:59:20 +08:00
|
|
|
|
bool forceCancel = true,
|
2026-06-16 17:34:00 +08:00
|
|
|
|
}) async {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
final url = '${HttpApiConsts.baseUrl}/iot/deviceTask/cancelTask';
|
|
|
|
|
|
final body = {
|
|
|
|
|
|
'taskId': taskId,
|
|
|
|
|
|
'siteId': siteId,
|
2026-09-10 08:58:04 +08:00
|
|
|
|
'orgId': orgId,
|
2026-09-01 08:59:20 +08:00
|
|
|
|
'forceCancel': forceCancel,
|
2026-08-07 08:49:29 +08:00
|
|
|
|
};
|
|
|
|
|
|
_logger.logWithLevel('[cancelTask] 请求: POST $url');
|
|
|
|
|
|
_logger.logWithLevel('[cancelTask] 参数: ${jsonEncode(body)}');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
try {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
final response = await dio.post(url, data: body);
|
|
|
|
|
|
_logger.logWithLevel('[cancelTask] 响应: statusCode=${response.statusCode}, body=${jsonEncode(response.data)}');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
|
|
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
|
|
final data = response.data as Map<String, dynamic>;
|
|
|
|
|
|
if (data['code'] == 200) {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
_logger.logWithLevel('[cancelTask] 结果: 成功');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
return data['data'] as bool? ?? false;
|
|
|
|
|
|
} else {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
_logger.logWithLevel('[cancelTask] 业务失败: code=${data['code']}, msg=${data['msg']}');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
throw Exception(data['msg'] ?? '取消任务失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw Exception('HTTP ${response.statusCode}');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
_logger.logWithLevel('[cancelTask] 异常: $e');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
rethrow;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Future<bool> pauseTask({
|
|
|
|
|
|
required String deviceId,
|
|
|
|
|
|
required int taskId,
|
|
|
|
|
|
required int orgId,
|
|
|
|
|
|
required int siteId,
|
|
|
|
|
|
}) async {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
final url = '${HttpApiConsts.baseUrl}/iot/deviceTask/pauseTask';
|
|
|
|
|
|
final body = {
|
|
|
|
|
|
'deviceId': deviceId,
|
|
|
|
|
|
'taskId': taskId,
|
|
|
|
|
|
'orgId': orgId,
|
|
|
|
|
|
'siteId': siteId,
|
|
|
|
|
|
};
|
|
|
|
|
|
_logger.logWithLevel('[pauseTask] 请求: POST $url');
|
|
|
|
|
|
_logger.logWithLevel('[pauseTask] 参数: ${jsonEncode(body)}');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
try {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
final response = await dio.post(url, data: body);
|
|
|
|
|
|
_logger.logWithLevel('[pauseTask] 响应: statusCode=${response.statusCode}, body=${jsonEncode(response.data)}');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
|
|
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
|
|
final data = response.data as Map<String, dynamic>;
|
|
|
|
|
|
if (data['code'] == 200) {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
_logger.logWithLevel('[pauseTask] 结果: 成功');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
return data['data'] as bool? ?? false;
|
|
|
|
|
|
} else {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
_logger.logWithLevel('[pauseTask] 业务失败: code=${data['code']}, msg=${data['msg']}');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
throw Exception(data['msg'] ?? '暂停任务失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw Exception('HTTP ${response.statusCode}');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
_logger.logWithLevel('[pauseTask] 异常: $e');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
rethrow;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Future<Map<String, dynamic>> recoveryTask({
|
|
|
|
|
|
required String deviceId,
|
|
|
|
|
|
required int taskId,
|
|
|
|
|
|
required int orgId,
|
|
|
|
|
|
required int siteId,
|
|
|
|
|
|
}) async {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
final url = '${HttpApiConsts.baseUrl}/iot/deviceTask/recoveryTask';
|
|
|
|
|
|
final body = {
|
|
|
|
|
|
'deviceId': deviceId,
|
|
|
|
|
|
'taskId': taskId,
|
|
|
|
|
|
'orgId': orgId,
|
|
|
|
|
|
'siteId': siteId,
|
|
|
|
|
|
};
|
|
|
|
|
|
_logger.logWithLevel('[recoveryTask] 请求: POST $url');
|
|
|
|
|
|
_logger.logWithLevel('[recoveryTask] 参数: ${jsonEncode(body)}');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
try {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
final response = await dio.post(url, data: body);
|
|
|
|
|
|
_logger.logWithLevel('[recoveryTask] 响应: statusCode=${response.statusCode}, body=${jsonEncode(response.data)}');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
|
|
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
|
|
final data = response.data as Map<String, dynamic>;
|
|
|
|
|
|
if (data['code'] == 200) {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
_logger.logWithLevel('[recoveryTask] 结果: 成功, data=${jsonEncode(data['data'])}');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
return data['data'] as Map<String, dynamic>? ?? {};
|
|
|
|
|
|
} else {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
_logger.logWithLevel('[recoveryTask] 业务失败: code=${data['code']}, msg=${data['msg']}');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
throw Exception(data['msg'] ?? '恢复任务失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw Exception('HTTP ${response.statusCode}');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
_logger.logWithLevel('[recoveryTask] 异常: $e');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
rethrow;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-06 08:41:44 +08:00
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Future<bool> saveDeviceSchedulePlan({
|
|
|
|
|
|
required DeviceSchedulePlanEntity plan,
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
final url = '${HttpApiConsts.baseUrl}/iot/deviceTask/save';
|
|
|
|
|
|
final body = _schedulePlanBody(plan);
|
|
|
|
|
|
_logger.logWithLevel('[saveDeviceSchedulePlan] 请求: POST $url', shouldLog: true);
|
|
|
|
|
|
_logger.logWithLevel('[saveDeviceSchedulePlan] 参数: ${jsonEncode(body)}', shouldLog: true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
final response = await dio.post(url, data: body);
|
|
|
|
|
|
_logger.logWithLevel('[saveDeviceSchedulePlan] 响应: statusCode=${response.statusCode}, body=${jsonEncode(response.data)}', shouldLog: true);
|
|
|
|
|
|
|
|
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
|
|
final data = response.data as Map<String, dynamic>;
|
|
|
|
|
|
if (data['code'] == 200) {
|
|
|
|
|
|
// 保存接口返回数量(如 data: 1),同时兼容布尔值及无 data 的成功响应。
|
|
|
|
|
|
final saved = switch (data['data']) {
|
|
|
|
|
|
bool value => value,
|
|
|
|
|
|
num value => value > 0,
|
|
|
|
|
|
_ => true,
|
|
|
|
|
|
};
|
|
|
|
|
|
_logger.logWithLevel(
|
|
|
|
|
|
'[saveDeviceSchedulePlan] 结果: ${saved ? '成功' : '未保存'}',
|
|
|
|
|
|
shouldLog: true,
|
|
|
|
|
|
);
|
|
|
|
|
|
return saved;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
_logger.logWithLevel('[saveDeviceSchedulePlan] 业务失败: code=${data['code']}, msg=${data['msg']}', shouldLog: true);
|
|
|
|
|
|
throw Exception(data['msg'] ?? '创建定时计划失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw Exception('HTTP ${response.statusCode}');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
_logger.logWithLevel('[saveDeviceSchedulePlan] 异常: $e', shouldLog: true);
|
|
|
|
|
|
rethrow;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 业务实体 → 后端报文:字段名 / DAY・WEEK・MONTH 枚举字面量 /
|
|
|
|
|
|
/// days・dates 条件结构 / 时间格式,全部封在数据层,不外泄到 domain。
|
|
|
|
|
|
static Map<String, dynamic> _schedulePlanBody(DeviceSchedulePlanEntity p) {
|
|
|
|
|
|
// 下标 = weekday.value - 1(周一~周日)
|
|
|
|
|
|
const List<String> weekdayWire = <String>[
|
|
|
|
|
|
'MONDAY',
|
|
|
|
|
|
'TUESDAY',
|
|
|
|
|
|
'WEDNESDAY',
|
|
|
|
|
|
'THURSDAY',
|
|
|
|
|
|
'FRIDAY',
|
|
|
|
|
|
'SATURDAY',
|
|
|
|
|
|
'SUNDAY',
|
|
|
|
|
|
];
|
|
|
|
|
|
String pad(int value) => value.toString().padLeft(2, '0');
|
|
|
|
|
|
String dateText(DateTime d) =>
|
|
|
|
|
|
'${d.year}-${pad(d.month)}-${pad(d.day)}';
|
|
|
|
|
|
|
|
|
|
|
|
return <String, dynamic>{
|
|
|
|
|
|
'deviceId': p.deviceId,
|
|
|
|
|
|
'orgId': p.orgId,
|
|
|
|
|
|
'planName': p.planName.trim(),
|
|
|
|
|
|
'routeId': p.routeId,
|
|
|
|
|
|
'siteId': p.siteId,
|
|
|
|
|
|
'deviceTaskPlanRules': <String, dynamic>{
|
|
|
|
|
|
'type': switch (p.type) {
|
|
|
|
|
|
WorkScheduleType.day => 'DAY',
|
|
|
|
|
|
WorkScheduleType.week => 'WEEK',
|
|
|
|
|
|
WorkScheduleType.month => 'MONTH',
|
|
|
|
|
|
},
|
|
|
|
|
|
if (p.type == WorkScheduleType.week)
|
|
|
|
|
|
'days': <String>[
|
|
|
|
|
|
for (final Weekday w in p.days) weekdayWire[w.value - 1],
|
|
|
|
|
|
],
|
|
|
|
|
|
if (p.type == WorkScheduleType.month) 'dates': p.dates,
|
|
|
|
|
|
'dayTime': '${pad(p.dayHour)}:${pad(p.dayMinute)}:00',
|
|
|
|
|
|
},
|
|
|
|
|
|
'startTime': '${dateText(p.startDate)} 00:00:00',
|
|
|
|
|
|
'finishTime': '${dateText(p.endDate)} 23:59:59',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-06-16 17:34:00 +08:00
|
|
|
|
}
|